rlsbl v0.113.0 /rlsbl.config
On this page

Project configuration loading with layered precedence from per-package, releasable, workspace, and project-level config.json files.

#rlsbl.config

#rlsbl.config

Project configuration loading with layered precedence from per-package, releasable, workspace, and project-level config.json files.

Layers (highest to lowest priority):

  1. Per-package config.json
  2. Releasable config.json

CLI flags override project-level .rlsbl/config.json which overrides user-level defaults.

#merge_config

python
def merge_config(base, overlay)

Merge two config dicts with shallow-replace, deep-merge for nested dicts.

Top-level keys in overlay replace those in base, except when both values are dicts -- in that case the nested dict is merged recursively (overlay nested keys merge into base nested keys).

Keys present in base but absent in overlay are preserved.

Returns a new dict; neither input is mutated.

#load_env_file

python
def load_env_file(path)

Load KEY=VALUE pairs from a file into os.environ.

Supports ~ expansion. Ignores comments (#) and blank lines. Strips surrounding quotes from values.

A configured file that does not exist is a HARD error. It used to print a warning and return, so a release whose env_file had moved (or was never present on this machine) went on to deploy, run its post-release hooks and drive local publish pipelines with none of the credentials the operator declared -- failing far downstream, after the tag and the GitHub Release, with an error naming a missing token instead of the missing file that explains it.

#_project_config

python
def _project_config(project_root)

Resolve project config path at call time.

Returns an absolute path based on project_root.

#read_json_config

python
def read_json_config(path)

Safely read a JSON file, returning {} on missing.

#should_tag

python
def should_tag(flags, config)

Returns True if tagging is enabled, checking flag > project > user > default.

config is the project config dict (already loaded). User-level config is still read from disk.

#read_project_config

python
def read_project_config(project_root, releasable_config_dir=None)

Read project config with optional releasable-level inheritance.

When releasable_config_dir is provided (path to a releasable's state directory, e.g. .rlsbl-monorepo/releasables/www/), config is loaded with 2-level precedence:

  1. Per-package config.json (highest)
  2. Releasable config.json (lowest)

When releasable_config_dir is None, loads only the per-package level.

#read_deploy_config

python
def read_deploy_config(config)

Read and validate deploy targets from project config dict. Returns (targets, errors).

#get_changelog_validation_config

python
def get_changelog_validation_config(config)

Read changelog validation config from a project config dict.

Returns the batch_limits section as a dict like {"max_commits_per_entry": 5, "max_entries_per_commit": 2, "exclusions": [{"reason": "...", "commits": [...], "entries": [{"version": "...", "line": N}]}]}.

Each exclusion object has a required "reason" string for audit purposes; "commits" and "entries" are optional lists silencing the corresponding batch_size_commits and batch_size_entries violations.

Returns an empty dict when batch_limits is absent. A present but non-dict batch_limits is a hard error (:class:ConfigError) -- never silently treated as absent.

#old_private_key_message

python
def old_private_key_message()

Exact-edit remediation for the removed private config key.

The private key was misleading (it read as GitHub repo visibility but meant "suppress publishing"). It is replaced by the publish_mode enum.

#get_publish_mode

python
def get_publish_mode(config)

Return the publish_mode enum value (one of :data:PUBLISH_MODES).

Single source of truth for reading the publish mode. Raises :class:ConfigError when the deprecated private key is present, when publish_mode is absent (it is required, no default), or when its value is not one of the valid modes.

#suppresses_publish

python
def suppresses_publish(config)

True when the config's publish_mode suppresses publishing ("none").

Derives the old is_private boolean from the enum. Raises :class:ConfigError via :func:get_publish_mode when the key is absent or invalid (required-read, no silent default).

#empty_targets_ban_message

python
def empty_targets_ban_message(location)

Return the standard error message for a banned empty targets list.

location describes where the empty list was found (e.g. "config" or a config file path). Shared so every call site emits an identical message.

#non_list_targets_ban_message

python
def non_list_targets_ban_message(location, value)

Return the standard error message for a non-list targets value.

A present-but-non-list targets (string, dict, ...) is a hard error -- never silently treated as absent.

#validate_config_schema

python
def validate_config_schema(config, *, project_dir=None)

Consolidated config schema validation -- single entry point for all banned keys and structural invariants.

Checks:

  1. publish_mode -- hard error if the deprecated private key is

present, if publish_mode is absent, or if its value is invalid.

  1. targets: [] -- hard error if targets key exists and is an empty

list. Use publish_mode: "none" to suppress publishing instead.

  1. release.mode -- hard error if the key exists. PR mode was

removed; even mode = "imperative" is dead config.

Called early in the release flow before any mutations.

Args:

  • config: the project config dict.
  • project_dir: unused (kept for call-site compatibility).

Raises:

  • ConfigError on any violation.

#_detect_go_artifact_kind

python
def _detect_go_artifact_kind(project_root='.') -> str

Detect whether a Go project is a library or binary.

Returns "library" when the project has no package main entry points (pure module), "binary" otherwise. Gracefully falls back to "binary" when introspection fails (e.g. go not on PATH).

Lives in config.py (not commands.init_cmd) so config validation can reuse it for the artifact error-message suggestion without a commands->config import cycle.

#validate_pipelines_config

python
def validate_pipelines_config(config, project_root='.')

Validate the pipelines section of a project config.

Raises ConfigError if:

  • pipelines is present but not a dict
  • An entry is not a dict
  • An entry is missing type (str) or local (bool)
  • A go pipeline is missing artifact or its value is not

binary/library

  • assets is true but max_asset_size_mb is missing or not a positive int
  • custom_assets is present but max_asset_size_mb is missing or not a positive int
  • custom_assets entries are malformed (missing name or build)

project_root is used only to auto-detect a suggested artifact value for the go-pipeline error message.

python
def validate_pipeline_target_links(config)

Validate the target link field on each pipeline entry.

Pipelines and targets are configured separately-but-linked: every pipeline entry in the pipelines section must declare an explicit target field. There is no name-based inference -- the link is always declared. The field takes one of two shapes:

  • a target NAME (string) that the pipeline publishes for. The name must

match a target present in the config's targets list (string form or dict {"name": ...} form). A name matching no configured target is a dangling reference and a hard error.

  • null (None) -- a targetless publisher (e.g. a docs/site deploy that

publishes no release artifact for any target).

A pipeline missing the target key is a hard error naming the pipeline and both valid shapes. This validator is additive to validate_pipelines_config and mirrors its style.

This validator does NOT require every target to be referenced by a pipeline -- pipeline-less targets (e.g. plain/spec) are legal.

Raises ConfigError on any violation.

#validate_test_config

python
def validate_test_config(config)

Validate the optional test section of a project config.

The test section maps a release target name to a block of per-target test options::

{"test": {"pypi": {"markers": "not integration"}}}

Absent section or absent target key means "run everything" (today's behavior). Everything must be declared -- unknown targets and unknown inner keys are hard errors (no silent tolerance of typos like marker).

Only pypi.markers is recognized today; the shape is built so future per-target options (go tags, npm script selection) slot in without reshaping.

Raises ConfigError if:

  • test is present but not a dict
  • a target key is not a recognized test target
  • a target block is not a dict
  • an inner key is not a recognized option for that target
  • pypi.markers is present but not a string, or is an empty string

#_validate_pypi_test_block

python
def _validate_pypi_test_block(block)

Validate the test.pypi options block. See validate_test_config.

#_declared_target_names

python
def _declared_target_names(config)

Return the set of target names declared in config['targets'].

Target entries may be bare strings or {"name": ..., "path": ...} dicts. Returns an empty set when the key is absent (partial config) so the caller can skip the cross-reference guard rather than reject everything.

#_validate_scalar_map

python
def _validate_scalar_map(value, where)

Validate that value is a map of non-empty string keys to scalars.

#_validate_service

python
def _validate_service(name, svc, declared_targets)

Validate a single services entry. See :func:validate_services_config.

#validate_services_config

python
def validate_services_config(config)

Validate the services and test_env sections of a project config.

services is a map of service-name to a definition::

{"services": {"postgres": { "targets": ["go"], "image": "postgres:17", "ports": ["5432:5432"], "env": {"POSTGRES_USER": "test"}, "health": {"cmd": "pg_isready -U test", "interval": "10s", "timeout": "5s", "retries": 5}, "setup": {"commands": ["apt-get update && ..."], "verify_sql": "SELECT ..."} }}}

test_env is a sibling scalar map rendered into the CI test job's env (values may reference the service, e.g. a DSN pointing at localhost:5432). test_env attaches to the union of every service's targets, so it requires at least one declared service.

Every service must declare targets (the release-target CI workflows it is provisioned into) and image. Unknown keys at any level are hard errors. verify_sql and verify_cmd are mutually exclusive.

Absent services and test_env is valid (returns silently).

Raises ConfigError on any violation.

#_read_unreleased_commits

python
def _read_unreleased_commits(config_path)

Read commit hashes from unreleased.jsonl adjacent to config_path.

Returns a set of commit hash strings found in the "commits" arrays of all entries in unreleased.jsonl. Returns an empty set if the file does not exist or is empty.

#clean_stale_exclusions

python
def clean_stale_exclusions(config_path)

Remove stale batch_limits exclusions after release finalization.

Two kinds of exclusions become stale:

  1. Entry-level (have "entries" with version="unreleased"):

after finalization renames unreleased.jsonl to X.Y.Z.jsonl, these are dead references.

  1. Commit-level (have "commits" but no "entries"): stale when

ALL referenced commits are no longer in unreleased.jsonl (they were moved to a versioned file during finalization).

Returns the number of exclusions removed. Returns 0 and does not write to disk if nothing changed.

#update_last_build_release

python
def update_last_build_release(project_dir, version)

Store last_build_release version in .rlsbl/config.json for OTA validation.

#write_project_config

python
def write_project_config(key, value, project_root)

Write or update a key in .rlsbl/config.json (creates dir if needed).

Returns the updated config dict after writing to disk.

Search