Skip to content
rlsbl.workspace
On this page

Workspace data layer for monorepo support handling discovery, loading, saving, and resolution of workspaces from workspace.toml config.

#rlsbl.workspace

#rlsbl.workspace

Workspace data layer for monorepo support handling discovery, loading, saving, and resolution of workspaces from workspace.toml config.

#read_releasable_version

python
def read_releasable_version(workspace_root, releasable_name)

Read the version string from a releasable's version file.

Args:

  • workspace_root: path to the monorepo root.
  • releasable_name: name of the releasable.

Returns:

  • The version string (stripped of whitespace).

Raises:

  • WorkspaceError: if the version file does not exist or is empty.

#write_releasable_version

python
def write_releasable_version(workspace_root, releasable_name, version)

Write a version string to a releasable's version file atomically.

Creates the releasable directory if it does not exist. Writes to a temporary file in the same directory and then atomically replaces the target file via os.replace().

Args:

  • workspace_root: path to the monorepo root.
  • releasable_name: name of the releasable.
  • version: the version string to write.

#find_workspace_root

python
def find_workspace_root(start_path='.')

Walk up from start_path looking for a .rlsbl-monorepo/workspace.toml.

Returns the directory containing .rlsbl-monorepo/, or None if not found.

#load_workspace

python
def load_workspace(root)

Read and validate workspace.toml, returning a list of WorkspaceProject.

Each project has at least 'path' (str) and 'name' (str, defaults to basename of path). The returned WorkspaceProject instances support dict-like access for backward compatibility.

Raises FileNotFoundError if workspace.toml doesn't exist. Raises WorkspaceError on invalid structure.

#releasable_keys

python
def releasable_keys() -> frozenset

Every key a [[releasables]] table may carry.

Derived from :class:~rlsbl.workspace_types.Releasable's own fields rather than restated, so a field added to the model stops being unknown on the same edit that adds it.

#_unknown_keys

python
def _unknown_keys(table, known)

The keys of table outside known, in the order they were written.

#_refuse_unknown_keys

python
def _refuse_unknown_keys(table, known, *, surface, file_label)

Refuse a table carrying a key rlsbl does not know.

The message names the surface (which table, of what kind), the offending key, and the file it was read from -- everything an operator needs to find and delete the line. A tolerated unknown key is a line that was written and never read: the file would say something the tools never do.

#_refuse_releasable_unknown_keys

python
def _refuse_releasable_unknown_keys(index, raw)

Refuse a [[releasables]] table carrying a key the model lacks.

#is_root_path

python
def is_root_path(path) -> bool

Does path spell the repository root ("", ".", "./")?

#_declared_paths

python
def _declared_paths(data, projects)

The path spellings as written, aligned with projects by index.

#_validate_member_paths

python
def _validate_member_paths(data, projects)

Refuse a member list that does not give each path exactly one member.

Two members claiming one territory make ownership depend on declaration order, so both spellings of the collision are refused at load: identical paths, and paths that differ only in spelling (a and ./a are the same directory, and normalize to the same member path).

#validate_workspace_model

python
def validate_workspace_model(data, projects)

Enforce the workspace ownership model on a parsed workspace.toml.

Each condition below is a hard error carrying its own remedy, and they are reported in the order an operator can act on them:

  1. a key at the top level that is neither section -- first, because a

misspelled section header is why the sections below look wrong;

  1. an implicit-mode workspace (no [[releasables]]) -- because

every other remedy below is written for an explicit-mode workspace;

  1. more than one root member;
  2. two members whose paths normalize to the same territory;
  3. no root member;
  4. a watch key on any member;
  5. a subtree_remote key on any member;
  6. a dev_node key on any member;
  7. any other unknown key on a member table;
  8. an unknown key on a [[releasables]] table;
  9. a root member named anything but root;
  10. a non-root member named root;
  11. a releasable owning the root member with no explicit tag_format.

Structural facts about the member list (3-5) precede per-member key errors (6-9): a remedy for a stray key presumes the member list itself is sound. The retired keys (6-8) precede the generic unknown-key refusal (9) so each keeps its own remedy.

data is the raw parsed document (needed for the releasables section and for the paths as the operator spelled them), projects the already-built :class:WorkspaceProject list, whose paths are normalized.

#load_releasables

python
def load_releasables(root, projects=None)

Load releasable definitions from workspace.toml.

Reads and validates the [[releasables]] section, then validates that every releasable project has a valid releasable field referencing a defined releasable name (or false).

Args:

  • root: path to the monorepo root (containing .rlsbl-monorepo/).
  • projects: optional pre-loaded project list. If None, loads via

load_workspace(root).

Returns:

  • A list of Releasable instances.

Raises:

  • WorkspaceError if [[releasables]] is missing, or on invalid
  • releasable definitions or missing/invalid project releasable fields.

#_load_explicit_releasables

python
def _load_explicit_releasables(raw_releasables, projects)

Parse [[releasables]] section and validate project membership.

Every releasable project must have a releasable field that is either a string referencing a defined releasable name, or false.

#members_of

python
def members_of(releasable_name, projects)

Return the list of projects that belong to a given releasable.

Projects with releasable = "<name>" matching the given name are returned as members.

Args:

  • releasable_name: the releasable name to look up.
  • projects: list of WorkspaceProject or dict instances.

Returns:

  • List of projects that are members of the releasable.

#resolve_releasable_for_project

python
def resolve_releasable_for_project(proj, releasables)

Return the Releasable that a project belongs to, or None.

Looks up the project's releasable field and matches it against the list of releasables.

Args:

  • proj: WorkspaceProject or dict with at least name and optionally

releasable.

  • releasables: list of Releasable instances.

Returns:

  • The matching Releasable, or None if the project is not releasable
  • (releasable = false) or no match is found.

#mirror_remote_for

python
def mirror_remote_for(proj, releasables) -> str

The mirror destination a project's releasable declares, or "".

The ONE resolution of "is this member mirrored, and where": the binding lives on the releasable, so every reader that used to read a member's own subtree_remote asks this instead. A member outside every releasable, or one whose releasable declares no mirror, answers the empty string.

#mirrored_releasable_for

python
def mirrored_releasable_for(proj, releasables)

The mirrored :class:Releasable proj belongs to, or None.

#_get_releasable_value

python
def _get_releasable_value(proj)

Extract the releasable value from a project (WorkspaceProject or dict).

Returns str, False, or None. Does not validate -- just reads the raw value.

#_build_project_table

python
def _build_project_table(d)

Build a fresh tomlkit table for a project dict.

Key order: path, name, then all remaining keys sorted. This matches the layout used when scaffolding a brand-new workspace.toml.

#_build_releasable_table

python
def _build_releasable_table(d)

Build a fresh tomlkit table for a releasable desired-dict.

d carries name and (optionally) tag_format and subtree_remote.

#_update_table_fields

python
def _update_table_fields(table, desired)

Update a tomlkit table in place to match desired (a plain dict).

  • Existing keys are reassigned only when their value actually changed

(so untouched keys keep their original formatting and inline comments).

  • New keys are appended (preserving the order of already-present keys).
  • Keys absent from desired are removed.

Intra-table comments attached to surviving keys are preserved by tomlkit.

#_sync_aot_in_place

python
def _sync_aot_in_place(aot, desired_list, id_key, build_fn)

Reconcile an existing tomlkit array-of-tables with a desired list.

Items are matched by identity (id_key: path for projects, name for releasables). Matched tables are updated field-by-field in place (preserving comments and key order). Tables whose identity is not in desired_list are removed. Desired items with no matching table are appended (in desired order) as fresh tables with a leading blank line so they read like the surrounding array-of-tables.

#_separate_releasables_from_projects

python
def _separate_releasables_from_projects(doc)

Keep a blank line between the two sections when one follows the other.

An appended array-of-tables entry carries a blank line BEFORE itself (trivia.indent), which separates it from its own siblings but not from whatever section comes next: a newcomer appended as the last [[releasables]] entry otherwise butts straight against the [[projects]] header. tomlkit holds the blank line AFTER a table as a trailing whitespace element inside that table, which is how a file that already reads well is recognized and left byte-for-byte alone -- a no-op save must not perturb a single byte.

#_member_to_write

python
def _member_to_write(proj)

The member dict save_workspace serializes, minus runtime bookkeeping.

Two things a load->save cycle must never do: persist a key a caller hung off the member at runtime (monorepo sync attaches its inlined-CI bookkeeping to the member dicts it walks), and write a key the loader would then refuse. The first is stripped -- those keys are the tools' own and were never part of the file -- and the second is a hard error here rather than an unreadable workspace.toml discovered on the next load.

#save_workspace

python
def save_workspace(root, projects, releasables=None)

Write workspace.toml atomically, editing the existing document in place.

When the file already exists it is parsed with tomlkit and the [[projects]] (and, when requested, [[releasables]]) arrays-of-tables are reconciled surgically: matched items (by path for projects and name for releasables) are updated field-by-field, absent items are removed, and new items are appended. Untouched tables, intra-table comments, key order, and every other top-level section are preserved byte-for-byte. When the file does not yet exist, a fresh document is created.

When releasables is passed (a list of Releasable instances), the [[releasables]] section is reconciled in place. When releasables is None, any existing [[releasables]] section is preserved untouched. Pass an empty list to write an explicitly empty section (releasables = []): a workspace with no releasables yet is still an explicit-mode workspace, and removing the section entirely would make it unreadable.

Creates .rlsbl-monorepo/ directory if it doesn't exist.

#resolve_project

python
def resolve_project(root, cwd='.')

Determine which project cwd is inside, returning a WorkspaceProject or None.

Uses the one path rule (:func:rlsbl.ownership.member_for_directory): the most specific declared member path wins, and the root member answers for every directory no other member claims -- including the repository root itself, which is exactly what a member declared at path = "." always matched.

None only when cwd is outside the workspace tree entirely. Inside a workspace, some member always answers, because a workspace always declares a root member.

#_derive_standalone_name

python
def _derive_standalone_name(project_root, detected_targets=None, targets_map=None)

Derive a project name for the standalone releasable.

Tries target read_name (first detected target), then falls back to the directory basename.

Args:

  • project_root: path to the project root (str or Path).
  • detected_targets: pre-detected list of TargetEntry instances.
  • targets_map: dict mapping target names to target objects.

Returns:

  • A non-empty name string.

#load_standalone_releasable

python
def load_standalone_releasable(project_root)

Load an explicit releasable definition from .rlsbl/releasable.toml.

If the file exists, reads name and tag_format from it. If absent, returns None (caller should use create_standalone_releasable).

Args:

  • project_root: path to the project root (str or Path).

Returns:

  • A Releasable instance, or None if the file does not exist.

Raises:

  • WorkspaceError on invalid file contents.

#save_standalone_releasable

python
def save_standalone_releasable(project_root, releasable)

Write .rlsbl/releasable.toml for releasable.

tag_format is written only when the releasable declares one, so a load -> save cycle neither invents the key on a file that stated none nor drops it from a file that did.

#create_standalone_releasable

python
def create_standalone_releasable(project_root)

Return a Releasable representing a single-project repo.

If .rlsbl/releasable.toml exists, uses its explicit configuration. Otherwise, derives the name from the project's target metadata (e.g., pyproject.toml [project].name) or the directory basename, and uses the standalone tag format (v{version}).

This function does NOT create any files on disk -- the releasable is purely an internal abstraction.

Args:

  • project_root: path to the project root (str or Path).

Returns:

  • A Releasable instance.

More tools from this site

  • claudestream Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python
  • claudewheel A TUI Claude Code Launcher that lets you have more than one profile, manage sessions lifecycle, pick the exact CC version, model to use (even older unlisted ones), pick which GitHub account to use, etc.
  • dirstat Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON
  • fastware A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client
  • go-toml-edit Zero-dep TOML editing library for Go with comment preservation
  • howmuchleft The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms
  • orxtra
  • pgdesign
  • predraw Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines
  • reposummary Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM
  • safegit git wrapper CLI that gives each commit its own temporary index and retries ref updates on conflict, so concurrent agents share one repository
  • saferm Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored
  • selfdoc Static Site Generator that builds a project's documentation site directly from its source code, so the docs can never drift from the code they describe, with SEO/AEO, first-class blog, search, and cross-project linking built in
  • strictcli
  • stricttest An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.
  • wesktop A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview
Search