rlsbl v0.113.0 /Configuration reference
On this page

Configuration reference: config.json pipelines, batch limits, the sandboxed test runner, internal dependency floors, freeform external_checks, the path-capable checks block, and workspace.toml.

#Configuration reference

#.rlsbl/config.json

Project-level configuration file created by rlsbl config init or rlsbl scaffold. This JSON file controls release behavior such as which targets to use (chosen from 18 supported registries) and whether ecosystem tagging is enabled. Settings here override user-level defaults in ~/.rlsbl/config.json but are themselves overridden by CLI flags passed at release time, forming a 3-layer precedence chain (CLI > project > user).

.rlsbl/config.json
KeyTypeValue
publish_modestring"ci"
env_filestring"~/Projects/.env"
push_timeoutinteger120
batch_limitsobject{...} (1 keys)
targetsarray[...] (2 items)
pipelinesobject{...} (2 keys)
changelog_format_version_enforcedbooleantrue
internal_dep_floorsarray[...] (3 items)
test_sandboxobject{...} (6 keys)

#Key reference

Key reference
KeyTypeDescription
targetsarrayList of target names to use for versioning (overrides auto-detection)
pipelinesobjectPublish pipelines keyed by user-chosen name (see Pipeline config below)
tagboolEnable/disable ecosystem tagging (default: true)
publish_modestring (required)Publish routing: "ci" publishes via CI pipelines; "none" suppresses publishing to public registries. No default. Replaces the former private boolean (a config still carrying private is a hard error).
release_branchesarrayBranch names that trigger the manual-release-push warning. Default: ["main", "master"] when absent. An empty list is a hard error -- either omit the key entirely or list at least one branch.
changelog_formatstringControls the format of generated CHANGELOG.md. Default: "grouped". Currently the only supported value, which produces version sections with ### Breaking, ### Features, ### Fixes sub-headers.
batch_limitsobjectLimits and exclusions for changelog batch-size validation checks. See batch_limits below.
check_timeoutintTimeout in seconds for check subprocesses (built-in tests, etc.). Default: 900. A declared budget, not a bypass — the check still hard-fails on a real hang.
push_timeoutintTimeout in seconds for each git push during a release. Default: 300. Overridable per-invocation with --push-timeout.
hook_timeoutintTimeout in seconds for release hooks. Default: absent, meaning no timeout — hooks run to completion.
ci_timeoutintTimeout in seconds for the release CI gate — the in-process wait for CI to conclude on the pushed release candidate. Default: 3600. Run discovery is spent inside this budget and capped at half of it, so the completion wait always keeps at least half. Overridable per-invocation with --ci-timeout.
build_timeoutint or objectTimeout in seconds for target build steps. An int applies to every target; an object is keyed by target name with an optional default entry. Falls back to each target's shipped default (120s for most, 300s for maven, 60s for pgdesign).
testobjectPer-target test-selection filters. See test below.
external_checksarrayConfig-declared freeform subprocess checks that run during rlsbl check and the release preflight. See external_checks below.
checksmapPer-check settings for the path-capable built-in tool checks (lint, format, type-check). See checks below.
strictspec_gateobjectOpt-in strictspec certificate deploy gate. Consumes a strictspec diff certificate as a format_version gate. See below.
test_sandboxobjectOpt-in sandboxed test runner. Declaring it makes rlsbl scaffold emit an executable bubblewrap runner script and turns on the stricttest-floor check. See below.
internal_dep_floorsarrayPackage names of ecosystem-internal dependencies whose declared >= floor must keep up with the locked version. Declaring the key turns on the [dep-floors](#internal_dep_floors) check. See below.

Configuration precedence for tagging: CLI flag (--no-tag) > project config > user config (~/.rlsbl/config.json) > default (true).

#strictspec_gate

The strictspec_gate object opts a project into the strictspec certificate deploy gate, a built-in preflight check (strictspec-certificate-gate) that consumes a [strictspec diff](https://github.com/smm-h/strictspec) certificate as a format_version deploy gate. Projects without this section are untouched — the check skips.

strictspec_gate
FieldTypeRequiredDescription
certificatestringYesPath (relative to the project root) to the strictspec diff certificate JSON. A missing or malformed file is a hard error.
adjudicationstringNoPath to a committed adjudication file (a gated strictspec TOML document) that discharges unsupported claims for a no-corpus consumer.

The gate applies strictspec's evidence-grade rule (decision 25):

  • a claim graded violated (a corpus document IS the counterexample) blocks the release;
  • corpus-supported and proven are the green light;
  • any other (unsupported) claim must be discharged by a matching entry in the adjudication file — an unsupported, unadjudicated claim blocks. There is no bypass.

The certificate itself is un-gated by design (it carries certificate_format_version, not a document format_version), so rlsbl parses it as plain JSON and inspects the claim grades. The adjudication file, by contrast, is a gated strictspec document and is validated against rlsbl's shipped adjudication schema.

{} json
{
  "strictspec_gate": {
    "certificate": ".rlsbl/strictspec/certificate.json",
    "adjudication": ".rlsbl/strictspec/adjudication.toml"
  }
}

#test_sandbox

The test_sandbox object opts a project into the sandboxed test runner rlsbl distributes: the outer layer of the stricttest test-isolation floor. Declaring the section makes rlsbl scaffold render the shared runner template to runner_path (executable), and turns on the stricttest-floor check. Projects without the section are untouched — the check skips.

Inside the sandbox the real repo is bound read-only, the suite runs in a writable throwaway copy of the tree on a private tmpfs, HOME is throwaway, and there is no network at all — a stray push, an unanchored commit into the dev repo, or a live API call is physically impossible rather than merely discouraged. The runner exports STRICTTEST_SANDBOX=1, the variable the stricttest plugin reads to lift its bare-run refusal.

test_sandbox
FieldTypeRequiredDescription
runner_pathstringYesWhere the runner is emitted, relative to the project root (e.g. scripts/test.sh).
commandstringYesThe command run inside the sandbox. Extra CLI arguments are appended to it. Must not contain a single quote (it is embedded in a single-quoted shell literal).
default_argsstringNoArguments used when the runner is invoked with none (e.g. -q -n auto).
cachesarrayNoToolchain caches to make available, from the closed set uv, go, python_user_base. An unlisted ecosystem costs nothing and requires none of its tools to be installed. Unknown names are a hard error, never a silently-ignored bind.
prewarmarrayNoShell commands run OUTSIDE the sandbox (network allowed) from the project root, before the sandbox is entered — for warming a cache the offline in-sandbox build then hits. A non-zero exit aborts the run.
extra_envobjectNoAdditional environment variables exported inside the sandbox, as a name-to-value map. STRICTTEST_SANDBOX is always exported and cannot be redeclared here.
ci_workflowsarrayNoWorkflow files that must invoke the runner. The stricttest-floor check hard-fails when a listed workflow does not — a repo that claims CI runs the suite sandboxed, but does not, is broken.
{} json
{
  "test_sandbox": {
    "runner_path": "scripts/test.sh",
    "command": "uv sync --offline && uv run --offline pytest",
    "default_args": "-q -n auto",
    "caches": ["uv", "go", "python_user_base"],
    "prewarm": ["scripts/test-prewarm.sh"],
    "ci_workflows": [".github/workflows/ci-pypi.yml"]
  }
}

Run <runner_path> --selftest to prove the invariants (the real repo is read-only, the network is dead) without running the suite.

#internal_dep_floors

The internal_dep_floors array opts a project into the dependency floor convention: when a release ships work that requires new behavior from a sibling framework package, the manifest must carry a >= floor at that version. The development lock already resolves the new version, so the repo's own suite passes — but a consumer installing the published artifact resolves whatever the declared floor allows, and gets an older framework that lacks the behavior. Declaring the key turns on the dep-floors preflight check. Projects without the key are untouched — the check skips.

The value is a list of package names to enforce. In a monorepo, every workspace sibling's package name is enforced too, without being listed — the workspace graph already knows which dependencies are ecosystem-internal.

internal_dep_floors
EcosystemDeclared floorLocked version
pypipyproject.toml [project].dependencies and optional-dependenciesuv.lock
npmpackage.json dependencies, peerDependencies, optionalDependenciespackage-lock.json
gogo.mod requirego.mod (same file)

Go is structurally satisfied and carries no comparison: a require line is the declared minimum, and the toolchain builds by minimal version selection, so the build can never sit ahead of the floor.

The check errors when the lock resolves an enforced dependency and either:

  • the manifest declares it with no readable >= floor, or
  • the locked version's major.minor exceeds the declared floor's major.minor.

Patch drift above the floor is fine — only a minor or major boundary is a behavior boundary. A name that appears only in the lock is transitive and is not this project's floor to declare. The error names the dependency, the locked version, and the exact constraint to write. Floors are not pins; upper bounds stay banned.

{} json
{
  "internal_dep_floors": ["strictcli", "stricttest", "selfdoc"]
}

#batch_limits

The batch_limits object controls the batch_size_commits and batch_size_entries changelog validation checks, which prevent excessively large changelog entries that obscure individual change attribution. Both checks produce blocking errors when they fail, and violations must be resolved before releasing. Default limits allow a maximum of 5 commits per entry and 5 entries per commit.

batch_limits
FieldTypeDefaultDescription
max_commits_per_entryint5Maximum number of commit hashes allowed in a single JSONL entry. Entries exceeding this limit fail the batch_size_commits check.
max_entries_per_commitint5Maximum number of JSONL entries that may reference the same commit hash. Commits exceeding this limit fail the batch_size_entries check.
exclusionsarray[]Per-violation silencers for known exceptions (see below).

Each entry in exclusions is a dict with:

batch_limits
FieldTypeRequiredDescription
reasonstringYesMandatory audit trail explaining why this exclusion exists.
commitsarrayAt least one of commits or entriesCommit hashes to exclude from the batch_size_entries check.
entriesarrayAt least one of commits or entriesEntry identifiers (commit lists) to exclude from the batch_size_commits check.

Example:

{} json
{
  "batch_limits": {
    "max_commits_per_entry": 5,
    "max_entries_per_commit": 5,
    "exclusions": [
      {
        "reason": "Large refactor commit touches many changelog areas",
        "commits": ["a1b2c3d"]
      }
    ]
  }
}

#test

The optional test block selects which tests run during the built-in test step of a release, on a per-target basis. It maps a release target name to a block of per-target options, letting you scope a target's test run down to a chosen subset (for example, excluding slow integration tests from the PyPI run). It maps a target name to a block of per-target options:

{} json
{
  "test": {
    "pypi": {
      "markers": "not integration"
    }
  }
}
  • pypi.markers is passed to pytest as -m <markers>, restricting the run to matching tests. Only pypi.markers is recognized today; the shape is built so future per-target options (Go build tags, npm script selection) slot in without reshaping.
  • An absent test section — or an absent target key — means "run everything", byte-identical to the prior behavior.
  • Everything must be declared: unknown target names and unknown inner keys are hard errors (no silent tolerance of typos like marker), and an empty markers string is rejected.

This is a selection filter, not a gate bypass. It narrows the set of tests that run; it does not let a failing test pass or suppress test failures.

#external_checks

The external_checks array declares project-specific subprocess checks. Each check is selected by tag during rlsbl check --tag <tag> and during the release preflight; a non-zero exit is a hard failure with no bypass.

Every entry must declare kind = "freeform". The marker is mandatory on purpose: a check whose scope rlsbl cannot see (an opaque shell command) must be a deliberate, visible declaration rather than an accident. Missing kind, an unknown kind, or any unrecognized key on an entry is a hard error.

kind = "structured" is retired. It named a known tool plus a path list and had rlsbl compose the argv; that shape is now the built-in checks block. A config that still declares it is a hard error naming the built-in that replaced it.

Keys:

external_checks
KeyTypeRequiredDescription
namestringYesCheck name. Lowercase letters, digits, hyphens only ([a-z][a-z0-9-]*); glob metacharacters are rejected so a name can never pattern-match a built-in check.
tagstringYesThe check tag under which this check is selected (e.g. preflight).
kindstringYes"freeform" -- the only kind.
depends_onarrayNoNames of checks that must run before this one.
cwdstringNoWorking directory (absolute, or relative to the project root).

#kind = "freeform"

A freeform check runs an opaque command string through a shell. rlsbl does not understand its scope — the command is executed verbatim. Use this for anything the built-in tool checks do not cover (npm scripts, Go tests, custom validation scripts, uvx tools, etc.).

kind = "freeform"
KeyTypeRequiredDescription
commandstringYesShell command. Its first token must resolve on PATH (or be an absolute path) — validated eagerly at registration. Environment assignments must use the explicit env VAR=1 cmd prefix form.
{} json
{
  "external_checks": [
    {
      "name": "typecheck",
      "kind": "freeform",
      "command": "npm run check",
      "tag": "preflight"
    }
  ]
}

#Release-context environment

External checks and the built-in tool checks alike run with rlsbl's release context merged into their environment on top of the ambient one. rlsbl already knows the answers — a check must never re-derive them with its own git describe, which gets monorepo tag prefixes wrong.

Release-context environment
VariableValueAvailability
RLSBL_PROJECT_ROOTThe resolved project root (absolute). The one way an entry with a cwd override can find it.Always
RLSBL_LAST_TAGThe project's last release tag, resolved through the same per-project tag glob the changelog layer uses (v* standalone, <name>@v* or the releasable's tag_format in a monorepo). The empty string when no tag exists, so "no baseline yet" is distinguishable from "not injected".Always
RLSBL_UNRELEASED_RANGE<last_tag>..HEAD, or HEAD on a first release.Always

The three are resolved once per check run and shared by every check in it, so N checks never mean N git describe calls.

Availability matrix — plain rlsbl check vs the release preflight:

Release-context environment
ContextThe three vars aboveRLSBL_VERSION / RLSBL_PREV_VERSION / RLSBL_BUMP_TYPE / RLSBL_DESCRIPTION / RLSBL_PACKAGE
rlsbl check / rlsbl check --tag <t>Present. RLSBL_LAST_TAG is the current last tag.Absent — these are hook-only vars.
Release preflight (inside rlsbl release run)Present, and computed before the version bump and tag: RLSBL_LAST_TAG is still the previous release, and RLSBL_UNRELEASED_RANGE covers exactly the commits being released.Absent — hooks (.rlsbl/hooks/*.sh) get them; checks do not. Write a release-diff check against RLSBL_LAST_TAG instead of the version pair.

A release-diff gate is then a one-liner:

$_ bash
git show "$RLSBL_LAST_TAG:schema/contract.json" > /tmp/baseline.json

with the first-release case handled by testing [ -z "$RLSBL_LAST_TAG" ].

#Timeout

Every config-driven check resolves its subprocess timeout per run from the configured check budget — --check-timeout if passed, else the check_timeout key in .rlsbl/config.json, else the shipped default (900s). This is the same precedence every built-in check uses. The budget is a declared limit, not a bypass: the check still hard-fails on a real hang.

#checks

The checks map configures the three path-capable built-in checks: lint, format and type-check. Each runs one Python tool over a project-declared path list, invoked through the project's own environment. A check with no entry here skips.

{} json
{
  "checks": {
    "lint":       {"paths": ["mypackage", "tests", "scripts", "docs"]},
    "format":     {"paths": ["mypackage", "tests", "scripts", "docs"]},
    "type-check": {"paths": ["mypackage", "tests", "docs"]}
  }
}
checks
KeyTypeRequiredDescription
pathsarrayYesNon-empty list of files/directories. This is the single source of truth for the tool's scope.
cwdstringNoWorking directory (absolute, or relative to the project root).

The composed argv, with no shell (paths are discrete arguments -- no word-splitting, no glob surprises). uv run degrades to the right dependency group or extra, so a tool declared outside the default dev group still resolves:

checks
checktoolcomposed argv
lintruffuv run ruff check <paths...>
formatruffuv run ruff format --check <paths...>
type-checkmypyuv run mypy <paths...>

All three are Python-only (they require a pypi target) and carry the quality and preflight tags.

#Competing-scope guards

Each of the three is paired with a pure, fast check named <check>-scope-guard. It hard-errors when the tool's own config file carries scope that competes with the declared paths, because such config silently changes what actually gets checked:

  • mypy guard (type-check-scope-guard). mypy's files / packages / modules config keys are silently overridden by CLI paths — a scope declared there is dead but misleading. The guard reads pyproject.toml [tool.mypy], mypy.ini, .mypy.ini, and setup.cfg [mypy]; any of those keys present is an error.
  • ruff guards (lint-scope-guard, format-scope-guard). ruff's include / extend-include config keys silently narrow the directories passed explicitly on the CLI (confirmed on ruff 0.15.20). The guard reads pyproject.toml [tool.ruff], ruff.toml, and .ruff.toml; include or extend-include present is an error. exclude / extend-exclude / force-exclude are exempt — those are bypassed by explicit paths (loud over-inclusion, not silent under-scoping).

Because the guards are pure, they execute under rlsbl release run --dry-run while the tool checks themselves are listed.

#Pipeline config

The pipelines key configures how releases are published. It replaces the old publish key, which is now rejected during rlsbl release run. Pipelines are separate from targets: targets handle version bumps (which files to update), while pipelines handle publishing (where and how to distribute the release).

Each entry in pipelines is keyed by a user-chosen name and must have:

Pipeline config
FieldTypeRequiredDescription
typestringYesOne of the 9 built-in pipeline types (see below)
localboolYesWhether to publish from the developer machine. When false, CI handles publishing.
artifactstringYes (type go); optional for npm/pypiSelects the publish workflow variant. Go pipelines require "binary" or "library". npm/pypi pipelines accept "launcher" for wrapper-package publishing. See pipelines docs.
wrapsstringWhen artifact is "launcher"Name of the pipeline that produces the binary. Must reference a pipeline with artifact: "binary".
binary_sourcestringWhen artifact is "launcher"Where the launcher downloads binaries from. Only "github-release" is supported.
token_varstringNoEnv var name for the publish token. Each type has a default (e.g. NPM_TOKEN for npm).
username_varstringNoEnv var name for username auth (used by docker).
password_varstringNoEnv var name for password auth (used by docker).
assetsboolNoEnable building and uploading target-specific artifacts to GitHub Releases.
max_asset_size_mbintWhen assets or custom_assets is setMaximum artifact size in MB. Releases fail if exceeded.
custom_assetsarrayNoList of custom build artifacts (see below).

Built-in pipeline types: npm, pypi, go, deno, hex, maven, docker, cloudflare-pages

Pipeline types use different auth patterns:

  • Token-based (npm, pypi, go, deno, hex, maven): authenticate via a single env var specified by token_var
  • Credential-based (docker): authenticate via username_var and password_var
  • Other (cloudflare-pages): type-specific auth configured per pipeline

#Per-pipeline-type reference

Per-pipeline-type reference
TypeDefault token_varAuth patternPublish action
npmNPM_TOKENTokenRuns npm publish (or pnpm/yarn equivalent based on lockfile detection)
pypiPYPI_TOKEN (or TWINE_PASSWORD)Token / OIDCOIDC Trusted Publishing preferred; falls back to token-based upload via twine
goNoneNoneNotifies Go module proxy (GOPROXY=proxy.golang.org); no authentication required
denoDENO_TOKEN (or JSR_TOKEN)TokenRuns deno publish to JSR
hexHEX_API_KEYTokenRuns mix hex.publish to hex.pm
mavenMAVEN_TOKEN (or GITHUB_TOKEN)TokenRuns gradle or maven publish task to configured repository
dockerN/AUsername + PasswordAuthenticates via DOCKER_USERNAME + DOCKER_PASSWORD, then runs docker push
cloudflare-pagesNoneSelfdoc CLIUses the selfdoc CLI for deploy; no token needed locally (CF credentials sourced from env)

#custom_assets

The custom_assets field is a list of dicts, each with:

custom_assets
FieldTypeRequiredDescription
namestringYesOutput filename (must appear in $RLSBL_DIST_DIR after the build command runs)
buildstringYesShell command to execute. Receives $RLSBL_DIST_DIR env var pointing to the distribution directory.

The build command runs with $RLSBL_DIST_DIR set to a temporary distribution directory. The command must produce a file named name in that directory. After building, rlsbl verifies the file exists and checks its size against max_asset_size_mb. All custom asset files are then uploaded to the GitHub Release.

Example config:

{} json
{
  "pipelines": {
    "npm-publish": {
      "type": "npm",
      "local": false
    },
    "docker-push": {
      "type": "docker",
      "local": true,
      "username_var": "DOCKER_USER",
      "password_var": "DOCKER_PASS"
    },
    "binaries": {
      "type": "go",
      "local": false,
      "custom_assets": [
        {"name": "myapp-linux-amd64.tar.gz", "build": "make dist-linux"},
        {"name": "myapp-darwin-arm64.tar.gz", "build": "make dist-darwin"}
      ],
      "max_asset_size_mb": 50
    }
  }
}

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

#User-level configuration

The user-level configuration file at ~/.rlsbl/config.json provides personal defaults that apply across all rlsbl-managed projects on the machine. It uses the same JSON schema as the project-level config, but with lower precedence -- project-level settings always override user-level ones, and CLI flags override both.

Location: ~/.rlsbl/config.json

This optional file uses the same JSON format as the project-level .rlsbl/config.json. It provides personal defaults that apply to all rlsbl-managed projects when a given key is not set at the project level.

Precedence (highest to lowest):

  1. CLI flags (e.g., --no-tag)
  2. Project config (.rlsbl/config.json)
  3. User config (~/.rlsbl/config.json)
  4. Built-in defaults

The file is optional — a missing ~/.rlsbl/config.json is not an error. When absent, built-in defaults apply for any key not set at the project level.

Currently supported keys:

User-level configuration
KeyTypeDefaultDescription
tagbooltrueControls whether ecosystem tags (e.g., npm dist-tags) are created during release.

Example ~/.rlsbl/config.json:

{} json
{
  "tag": false
}

#CLI flag overrides

Some CLI flags override config.json keys for a single invocation, providing temporary behavior changes without modifying the persistent configuration. Most global flags like --dry-run, --approve-consequential, and --quiet are runtime-only and have no persistent config equivalent, affecting only the current command execution.

CLI flag overrides
FlagConfig keyScopeEffect
--no-tagtagproject + userDisables ecosystem tagging for this invocation
--allow-dirty(none)release onlySkips clean working tree check
--watch/--no-watch(none)release onlyControls CI monitoring after push
--push-timeoutpush_timeoutrelease onlyPush timeout in seconds for this invocation (0, the default, means use the config key, else the shipped 300s)
--ci-timeoutci_timeoutrelease onlyCI-gate timeout in seconds for this invocation (0, the default, means use the config key, else the shipped 3600s)
--check-timeoutcheck_timeoutrelease onlyCheck-subprocess timeout in seconds for this invocation, covering built-in checks, config-declared external checks and their scope guards (0, the default, means use the config key, else the shipped 900s)
--hook-timeouthook_timeoutrelease onlyRelease-hook timeout in seconds for this invocation (0, the default, means use the config key, else no timeout)

Global flags --dry-run, --approve-consequential, and --quiet are runtime-only and have no config.json equivalent. They affect the current invocation but are never persisted to configuration.

Timeouts are never read from the environment. RLSBL_PUSH_TIMEOUT, RLSBL_CHECK_TIMEOUT, RLSBL_HOOK_TIMEOUT, RLSBL_BUILD_TIMEOUT, and RLSBL_BUILD_TIMEOUT_<TARGET> were removed: a timeout is declared in .rlsbl/config.json or passed explicitly on the command line, never picked up from ambient environment state.

#.rlsbl-monorepo/workspace.toml

Monorepo workspace definition that lists all sub-projects, their relative paths, and optional names. rlsbl walks up from the current directory to find this file, so you can run release commands from within any sub-project. See the monorepo guide for setup instructions, workspace commands, and subtree publishing.

The file uses TOML format with a [[projects]] array. Each entry has a path key (relative to the monorepo root) and an optional name key. If name is omitted, the directory basename is used.

#selfdoc.json

When present in the project root, this file configures documentation builds via selfdoc. It specifies the source directories to scan, the output path for generated pages, the base URL for the published site, and an optional deploy provider such as Cloudflare Pages. Documentation deployment is handled via a cloudflare-pages pipeline in .rlsbl/config.json, not as a release target. See the selfdoc documentation for the full schema.

selfdoc.json
KeyTypeValue
base_urlstring"https://rlsbl.smmh.dev"
search_enginestring"pagefind"
authorobject{...} (3 keys)
sourcearray[...] (1 items)
docsstring"docs/"
outputstring"docs/_build/"
localesarray[...] (1 items)
genobject{...} (1 keys)
root_filesarray[...] (2 items)
directivesobject{...} (5 keys)
topologyobject{...} (4 keys)
postsobject{...} (1 keys)
assemblyobject{...} (1 keys)
Search