rlsbl v0.113.0 /rlsbl.tool_checks
On this page

How rlsbl invokes a checking tool: the lint, format and type-check built-ins, their declared path lists, the uv-run argv composition, and the competing-scope guards.

#rlsbl.tool_checks

#rlsbl.tool_checks

How rlsbl invokes a checking tool, and the three built-ins that do it.

Three built-in checks -- lint, format and type-check -- run a Python tool over a project-declared path list, through the project's own environment. They are configured in .rlsbl/config.json::

"checks": { "lint": {"paths": ["mypackage", "tests", "scripts", "docs"]}, "format": {"paths": ["mypackage", "tests", "scripts", "docs"]}, "type-check": {"paths": ["mypackage", "tests", "docs"]} }

A check with no entry skips. paths is required when an entry is present; cwd is optional and resolves against the project root.

Why the paths are declared rather than inferred -----------------------------------------------

Both tools read scope from their own config files, and both do it in a way that silently disagrees with an explicit CLI path list:

  • mypy's files / packages / modules are OVERRIDDEN by CLI paths --

a scope declared there is dead but reads as authoritative.

  • ruff's include / extend-include silently NARROW the directories

passed on the command line (measured on ruff 0.15.20).

So each tool check is paired with a competing-scope guard check (lint-scope-guard and friends) that hard-errors when the tool's own config carries scope. The guards are pure and fast, so they run in a preview while the tool checks themselves are listed.

exclude / extend-exclude / force-exclude are deliberately exempt: an explicit path bypasses them, which produces loud over-inclusion rather than silent under-scoping.

Invocation ----------

uv run [--group G | --extra E] <binary> [subcommand...] <paths...>, with no shell, in the project directory, with the release context in the environment. The group/extra flags are resolved by reading where the project declares the tool, so the common case reproduces a bare uv run <tool>.

#guard_name

python
def guard_name(check_name)

Name of the competing-scope guard paired with check_name.

#ToolCheckConfigError

Raised when the checks config block is invalid.

#validate_tool_checks_config

python
def validate_tool_checks_config(config)

Validate the checks section of a project config.

Returns {check_name: entry} for the declared checks, or {} when the key is absent. Every violation is a hard error: an unknown check name, a missing or empty paths, a non-string path, an unknown key.

#declared_entry

python
def declared_entry(config, check_name)

The validated entry for check_name, or None when it is not declared.

#probe_tool_location

python
def probe_tool_location(project_dir, tool_binary)

Detect where tool_binary is declared in a project's pyproject.toml.

Checks, in order:

  1. [dependency-groups].* -- any group declaring the tool
  2. [project.optional-dependencies].* -- any extra declaring the tool
  3. [tool.uv].dev-dependencies -- uv legacy dev deps

Returns (source_type, name) on match, else None. source_type is one of "dependency-group", "optional-dep", "uv-dev".

#resolve_tool_group_flags

python
def resolve_tool_group_flags(project_dir, tool_binary)

Return the uv run group/extra flags needed to reach tool_binary.

Degrades to [] (plain uv run) when the tool lives in the default dev group, in uv's legacy dev-dependencies, in a uv workspace venv, or cannot be located -- so the common case reproduces bare uv run <tool>. Non-default dependency groups yield ["--group", name] and optional extras yield ["--extra", name].

#compose_argv

python
def compose_argv(check_name, paths, project_dir)

Compose the shell-free argv for one tool check.

#release_context_env

python
def release_context_env(ctx)

Return the subprocess env for a check: os.environ + RLSBL_*.

Injected (see docs/configuration.md for the availability matrix):

  • RLSBL_PROJECT_ROOT -- the resolved project root. An entry with a

cwd override otherwise has no way to find it.

  • RLSBL_LAST_TAG -- the project's last release tag, resolved through

the same per-project tag glob the changelog layer uses (so it is monorepo-correct). The EMPTY STRING when no tag exists, so a check can tell "no baseline yet" from "not injected".

  • RLSBL_UNRELEASED_RANGE -- <last_tag>..HEAD, or HEAD on a

first release.

Computed once per check run and memoized on the context object.

#resolve_cwd

python
def resolve_cwd(ctx, cwd)

Resolve a check's declared cwd against the project root.

#report_subprocess_result

python
def report_subprocess_result(reporter, result, name)

Turn a completed subprocess into a pass/fail check result.

Every line handed to the reporter goes through reportable_lines: the reporter rejects empty problem text with an exception that propagates out of the whole check run, and real linters separate their findings with blank lines, so unfiltered output turned a lint failure into an unattributed internal error.

#resolve_check_budget

python
def resolve_check_budget(ctx)

Resolve the subprocess budget for a check at RUN time.

Read from the live ctx.config rather than bound when the check spec is built, so --check-timeout (which the release writes into its in-memory config) is honored. One precedence chain -- flag > check_timeout config key > shipped default -- governs every check.

#run_tool_check

python
def run_tool_check(ctx, reporter, check_name)

Run one declared tool check, or skip when it is not configured.

#mypy_scope_conflicts

python
def mypy_scope_conflicts(root)

Return a list of (source, key) where mypy config carries scope.

mypy's files/packages/modules config keys are silently OVERRIDDEN by CLI paths -- a scope declared there is dead but misleading. Checks pyproject [tool.mypy], mypy.ini, .mypy.ini and setup.cfg [mypy].

#ruff_scope_conflicts

python
def ruff_scope_conflicts(root)

Return a list of (source, key) where ruff config narrows scope.

ruff's include/extend-include config keys silently NARROW the directories passed explicitly on the CLI (confirmed on ruff 0.15.20). exclude/extend-exclude/force-exclude are exempt: they are bypassed by explicit paths (loud over-inclusion, not silent under-scoping). Checks pyproject [tool.ruff], ruff.toml and .ruff.toml.

#scope_conflicts

python
def scope_conflicts(check_name, root)

Competing-scope findings for check_name's tool under root.

#run_scope_guard

python
def run_scope_guard(ctx, reporter, check_name)

Run the competing-scope guard paired with check_name.

Search