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/modulesare OVERRIDDEN by CLI paths --
a scope declared there is dead but reads as authoritative.
- ruff's
include/extend-includesilently 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
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
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
def declared_entry(config, check_name)The validated entry for check_name, or None when it is not declared.
#probe_tool_location
def probe_tool_location(project_dir, tool_binary)Detect where tool_binary is declared in a project's pyproject.toml.
Checks, in order:
[dependency-groups].*-- any group declaring the tool[project.optional-dependencies].*-- any extra declaring the tool[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
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
def compose_argv(check_name, paths, project_dir)Compose the shell-free argv for one tool check.
#release_context_env
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, orHEADon a
first release.
Computed once per check run and memoized on the context object.
#resolve_cwd
def resolve_cwd(ctx, cwd)Resolve a check's declared cwd against the project root.
#report_subprocess_result
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
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
def run_tool_check(ctx, reporter, check_name)Run one declared tool check, or skip when it is not configured.
#mypy_scope_conflicts
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
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
def scope_conflicts(check_name, root)Competing-scope findings for check_name's tool under root.
#run_scope_guard
def run_scope_guard(ctx, reporter, check_name)Run the competing-scope guard paired with check_name.