On this page
Every check rlsbl runs, grouped by tag, with its severity, the targets it applies to, how a workspace run is scoped, and the checks strictcli registers.
#Check system
rlsbl includes 85 checks across 9 tags.
Run checks via the rlsbl check command. Checks are organized across 6 primary tags (project, release, changelog, workspace, quality, prepush) and validate project metadata, release state, changelog structure, workspace integrity, code quality, and pre-push enforcement. Four additional untagged checks run only with --all or --name. Three further tags exist for pipeline and target-specific grouping: preflight and preflight-changelog are run internally by rlsbl release run, and maven groups the Maven-specific check. A few more checks come from strictcli rather than from rlsbl's own checks.toml and are therefore outside every count on this page; they are documented under framework checks.
#Running checks
# Run all checks
rlsbl check --all
# Run all checks in a tag
rlsbl check --tag changelog
# Run a single check by name
rlsbl check --name version-consistency
# Scope a run to one releasable (required at a monorepo workspace root)
rlsbl check --tag changelog --releasable core#Where a check run is scoped
In a standalone repository, and in a workspace member's directory, the cwd names exactly one project and every check answers for it.
A workspace ROOT does not: it names the workspace. The checks that answer for one project -- the whole project, changelog and release families, and every quality check that is not workspace-scoped -- therefore refuse there with an error result naming both routes, rather than reading the root as a project of its own (which reported SKIP "no targets detected" and demanded config keys in a .rlsbl/config.json a workspace root must not have). Name the releasable with --releasable <name> to scope the run to it, or run rlsbl check from a member directory.
Two families are untouched at the root, because it is the position they are meant to be run from: the workspace-scoped checks (--tag workspace), and the prepush family, which the pre-push hook runs at the repository root of every workspace.
--releasable is refused anywhere the directory already answers -- a member directory, or a standalone repository.
check is strictcli's own auto-registered command and its flags are the framework's, so rlsbl lifts --releasable out of argv before the app parses it, exactly as it does for the positional arguments strictcli cannot express. It is therefore absent from rlsbl check --help and from the dumped CLI schema; this page and the refusal itself are where it is documented.
#Check results
Each check returns one of four statuses that determine how the result is displayed and whether it blocks the release pipeline. The severity level (error or warn) is declared per-check in the check metadata stored in checks.toml and controls which status is reported on failure versus advisory findings:
| Status | Meaning | Effect |
|---|---|---|
| pass | Check passed | No action needed |
| fail | Check failed | Blocking error -- must be fixed before release |
| warn | Advisory finding | Informational -- does not block release |
| skip | Not applicable | Check cannot run for this project (e.g., workspace check in a standalone project) |
Severity is declared per-check in metadata. A check with severity = "error" reports fail on failure; one with severity = "warn" reports warn.
#Tags
| Tag | Purpose | Check count |
|---|---|---|
project | Project-level metadata, config schema, version consistency | 27 |
release | Released-version refs, branch sync, CI credentials, and conversion follow-ups | 6 |
changelog | JSONL changelog validation and structure | 11 |
workspace | Monorepo workspace integrity and dependency rules | 19 |
quality | Code quality, dependency analysis, scaffold hygiene | 16 |
prepush | Pre-push enforcement: changelog coverage, gitignore guard, manual-push warning, tests | 6 |
Some checks carry multiple tags, so they appear in multiple tag counts: test-suite is tagged prepush and quality, test-suite-workspace is tagged prepush and workspace, and scaffold-conflicts is tagged project, prepush, and release. Four checks (layers-violations, deps-unused, deps-undeclared, deps-stale) have no tag and only run with --all or --name. Internal tags used by the release pipeline: preflight (21 checks: library-lint, test-suite, dev-overlay-drift, maven-central-metadata, wrapper-producer, strictspec-certificate-gate, stricttest-floor, dep-floors, dep-locks, go-module-identity, ldflags-symbol, strictspec-generated-floor, target-matrix-fresh, router-filters-fresh, workspace-unbuildable, and the six path-capable tool checks lint, lint-scope-guard, format, format-scope-guard, type-check, type-check-scope-guard) and preflight-changelog (9 checks: the structural changelog checks, i.e. all changelog checks except changelog-entry and changelog-format-version). The maven tag groups maven-central-metadata.
#Project checks
| Check | Severity | Description |
|---|---|---|
lock | warn | Detects stale lock state in .rlsbl/ |
version-consistency | error | Project version matches across all target files (e.g., pyproject.toml, package.json, .rlsbl/version) |
name-consistency | warn | Package name is consistent across manifest files |
description-consistency | warn | Package description is consistent across manifest files |
license-file | error | A LICENSE file exists in the project root |
license-consistency | warn | License identifier matches across manifest files |
config-schema | error | The three things .rlsbl/config.json may not say: the retired private key (publish_mode is required in its place), an empty targets list (publish_mode: "none" is how publishing is suppressed), and a release.mode key (PR mode was removed). Plus the shape of the pipelines section and its links to the declared targets. An unknown key is NOT policed here — see policed configuration surfaces |
private-hook-stale | error | Detects leftover private repo hook files that should be deleted |
publish-mode-workflow | error | publish_mode: "none" repos must not have a publish workflow that pushes to public registries |
npm-private-mismatch | error | package.json private field matches .rlsbl/config.json private flag (npm targets only) |
target-version-readable | error | Version can be read from all declared target files |
dunder-version-missing | error | PyPI targets that keep a version constant in source must use __version__ |
selfdoc-version-drift | error | selfdoc-generated version references match the actual project version |
scaffold-conflicts | error | Unresolved git merge conflict markers in scaffold files (managed-files registry, .github/workflows/, all of .rlsbl/); also tagged prepush and release |
stash-free | error | The repository carries no stash. A stash is uncommitted work with no branch of its own, so nothing records what it belongs to; rlsbl release run, rlsbl release resume, rlsbl release reconcile --apply and rlsbl release backfill --apply each refuse one outright, and this reports it before any of them is reached |
cross-repo-path-sources | error | [tool.uv.sources] path entries in the committed pyproject.toml must resolve inside the repository (in-repo paths and workspace = true are legal; local overrides belong in dev-sources.toml.local-only). Also enforced unconditionally by rlsbl release run |
dev-overlay-drift | error | Packages recorded in the rlsbl dev sync sentinel are still editable installs of their declared checkouts (a bare uv sync silently replaces an overlay with the released wheel) |
requires-services | error | CI service containers declared under services/test_env are actually provisioned in the rendered CI workflow |
wrapper-producer | error | Every launcher pipeline's wraps reference names a real binary-artifact pipeline, and the wrapped target's manifest still carries the shim-critical fields |
strictspec-certificate-gate | error | A configured strictspec diff certificate reports no violated (or unsupported-and-unadjudicated) claim. Skips when the project has no strictspec_gate section |
stricttest-floor | error | An adopted sandboxed test runner works: the test_sandbox runner script exists and is executable, the config family is complete, and every CI workflow the family names actually invokes the runner. Skips when the project has adopted neither the test_sandbox family nor the stricttest plugin |
dep-floors | error | Ecosystem-internal dependencies declare a >= floor at the version the lock resolves. Compares pyproject.toml against uv.lock and package.json against package-lock.json; Go is structurally satisfied (require lines are the minimums). Skips when the project has no internal_dep_floors config key |
dep-locks | error | Every lockfile still resolves the manifest beside it: uv.lock's entry for this project against pyproject.toml (version, requirements, dependency groups), package-lock.json's root entry against package.json, and go.sum's coverage of go.mod's requires. Structural and offline -- no package manager is invoked and nothing is resolved. Each finding names the command that refreshes the lock |
go-module-identity | error | Every go.mod's module path equals the repository's origin identity plus the module's subdirectory (git@github.com:owner/repo.git + services/api -> github.com/owner/repo/services/api), with a /vN major suffix accepted. Findings name the rlsbl rewrite go-module-path invocation that fixes them. Skips when there is no origin remote; when the remote names an SSH host alias rather than a domain, the host segment is left unverified and the outcome says so |
ldflags-symbol | error | Every -X importpath.Symbol=value linker flag in the project's tracked build configuration (.goreleaser.yml/.yaml, Makefiles, shell scripts, CI workflow YAML) names a symbol the Go source declares as a package-level var of type string, uninitialized or initialized to a constant string -- the only shape the linker can set. A -X naming a symbol that does not exist links SILENTLY, so the released binary reports its fallback version forever and nothing in the toolchain says so. Symbol-agnostic: main.version with var version and main.Version with var Version both pass; only the relationship is checked. A symbol that exists and is injectable but that nothing in the module reads WARNS instead -- the same user-visible bug by a different route, but fixing it can mean adding a version surface. .rlsbl/bases/ (scaffold base copies) and dist/ (goreleaser output) are not scanned, and an occurrence whose target cannot be resolved (a build-time template in the import path, a package outside this module) is reported as unverified rather than guessed at |
strictspec-generated-floor | error | The declared strictspec floor is at least the highest GENERATED_BY among the validators strictspec.toml declares. A generated validator calls require_runtime_version(GENERATED_BY) at import and that pairing is exact, so a lower floor ships an artifact whose validators cannot import on a floor-resolved install. Compared at full patch precision, which dep-floors (major.minor, against the lock) cannot see. Skips when the project has no strictspec.toml |
target-matrix-fresh | error | The committed support matrix (rlsbl/data/support-matrix.json) matches a fresh regeneration from the target, check and pipeline registries. The docs directives render from that file instead of importing rlsbl, so a stale file ships wrong documentation. Skips wherever the artifact does not exist |
#Release checks
| Check | Severity | Description |
|---|---|---|
unpublished-refs | error | Every ref each archived release owns -- its primary tag, its ecosystem's companion tags, and its recorded aliases (a rename's or conversion's boundary-alias event, and the historical spelling an archive records in shipped_as) -- exists locally, exists on origin, and points at the commit the release recorded; and every version whose primary tag is on origin carries a GitHub Release (requires network) |
branch-sync | error | Local branch is not behind the remote tracking branch (requires network) |
ci-publish-secrets | error | Every Actions secret the configured CI publish pipelines authenticate with exists on the repository. Which secrets those are is each pipeline's own declaration (ci_secret_names): the npm pipeline declares NPM_TOKEN, the maven-central pipeline declares SONATYPE_USERNAME, SONATYPE_PASSWORD, GPG_SIGNING_KEY and GPG_SIGNING_KEY_PASSWORD, the hex pipeline declares HEX_API_KEY, a pypi pipeline declares none because it publishes through OIDC trusted publishing, and a local: true pipeline declares none. Without the secret the publish job fails with ENEEDAUTH after the release has already tagged, pushed and created the GitHub Release. Presence only -- the value is never read. The finding names the exact gh secret set command (requires network) |
old-repo-archived | error | Every repository this one absorbed (from the transition record's conversion facts) is archived on GitHub, so it stops collecting issues, pull requests and clones for code that moved here. rlsbl never archives it -- the finding prints gh repo archive. Skips when the record holds no absorb (requires network) |
go-deprecation-published | error | Every superseded Go module path (from the transition record's identity transitions) serves a // Deprecated: notice in the go.mod the module proxy returns for its latest version. rlsbl never commits into the retired repository -- the finding prints the steps. Skips when the record holds no go-module-path transition (requires network) |
scaffold-conflicts (see project checks) is also tagged release. unpublished-refs depends on version-consistency; if it fails, unpublished-refs is skipped.
Every check in this tag reads something outside the working tree -- the remote's refs, the GitHub API, the Go module proxy. That is why they carry release rather than project: the offline tags (project, changelog, quality, prepush) stay answerable with no network, and a networked check placed in one of them would fail an offline run for a reason that has nothing to do with the repository. All of them are fail-closed: a probe that cannot answer is a hard error, never a pass. None is in preflight -- the release pipeline's own steps already authenticate against GitHub and the registries, and a probe failure there would block a release for a network condition rather than for anything about the repository.
#unpublished-refs
The ref set it renders against reality is expected_refs, the target protocol's single authority for what one version owns -- the same derivation the release's tag step acts on, so a ref the release creates can never be a ref the check does not look for.
Each failure it reports names rlsbl release reconcile as the remedy:
| Failure | What it means |
|---|---|
| missing locally | The release record records the version as released, but the ref is not in this repository |
| missing on origin | The ref exists locally but was never pushed, so consumers cannot resolve it |
| wrong commit | The ref exists but points somewhere other than the release's release commit, so it moved after the release wrote it |
| no GitHub Release | The version's primary tag is on origin, but no Release document hangs off it, so the forge shows consumers no notes and the publish workflow finds no rlsbl-ci-sha marker to judge |
It is fail-closed: a probe that cannot answer -- an unreadable local tag namespace, an ls-remote that fails, a Release listing that errors or that gh cannot make because it is absent or unauthenticated -- is an error, never a pass. A repository with no origin remote at all is a different state: the remote half is skipped and the outcome says so. So is a repository whose origin resolves to no GitHub repository: the Release half is skipped, because there is no forge for a Release to be missing from.
A version the release record records as unrecoverable has no recoverable commit, so a ref it is missing cannot be recreated and rlsbl release reconcile has nothing to point at -- and no release commit to take a rlsbl-ci-sha marker from either, so a Release it lacks cannot be materialized. Both absences are counted and named in the outcome message instead of reported as fixable errors. Every other finding for such a version -- a ref that exists locally but not on origin -- is still reported.
A version the release record records as never_released is a version NUMBER no release ever used, so it owns no ref and no Release at all. It is skipped, named in the outcome message, and excluded from the released-version count -- reporting a phantom as a release would be the check agreeing with the mistake it exists to surface.
A Release is judged on the version's primary tag, the one the release flow attaches it to, and only when that tag is on origin. A tag that never reached the forge is already reported as missing on origin, and a Release cannot exist without it.
No version window. Every half covers every archived release, not a recent slice. The whole local tag namespace comes from one for-each-ref, the whole remote namespace from one ls-remote, and every Release from one gh release list, so probing two hundred releases costs the same as probing one. A per-version probe would have forced a bound and left older releases unchecked.
It replaced three narrower checks (local-tag, remote-tag, github-release) that each looked at the primary tag of the current version only, and so saw neither companion tags, nor recorded aliases, nor any past release.
GitHub Release presence, and its repair. The retired github-release check asked whether the current version's Release exists; this check asks it of every archived version, from the same listing, and reports an absence as an error. The repair is rlsbl release reconcile: --plan gives such a version a materialize verdict, and --apply creates the Release with the same body the release flow itself writes (the changelog section, the rlsbl-ci-sha marker taken from the recorded release commit, and the pre-release flag the version earns). The check finds the gap; the reconcile closes it. See Reconciling published metadata.
#Changelog checks
| Check | Severity | Description |
|---|---|---|
changelog-hashes | error | Every commit hash in JSONL entries resolves via git rev-parse |
changelog-range | error | Every resolved hash falls within the unreleased range -- the commits after this checkout's nearest release commit. In a workspace, a hash in that range owned by another releasable is reported as out of SCOPE, naming that owner, rather than as out of range |
changelog-coverage | error | Every unreleased commit appears in at least one JSONL entry |
changelog-orphans | error | No entries whose every hash is unresolvable, out of range, or owned by another releasable (stale from rebased/amended commits, or cross-filed) |
changelog-schema | error | User-facing entries have description and type; type is one of feature/fix/breaking |
changelog-user-facing | warn | At least one entry is user-facing (hard error during release, warning in check mode) |
changelog-batch-commits | error | No single entry references more commits than max_commits_per_entry (default 5) |
changelog-batch-entries | error | No single commit appears in more entries than max_entries_per_commit (default 5) |
changelog-entry | warn | CHANGELOG.md contains an entry for the current project version |
changelog-format-version | warn | The repo has recorded a changelog_format_version_enforced decision (enabling the gate below, or staying in legacy mode deliberately) |
changelog-format-version-gate | error | When enforcement is on, every line in unreleased.jsonl and every finalized x.y.z.jsonl carries a supported format_version. Skipped while enforcement is off |
Dependencies: changelog-range and changelog-coverage depend on changelog-hashes (hash resolution must succeed first).
#Workspace checks
| Check | Severity | Description |
|---|---|---|
router-filters-fresh | error | The filters: block of the generated ci-router.yml matches a fresh derivation from the workspace: each member's own territory, the territories of everything it depends on (transitively, every scope), the workspace-root manifests and lockfiles, the tool's own machinery, and -- for the root member -- ** narrowed by negated excludes of every other territory. Also verifies the step declares predicate-quantifier: some-with-excludes, without which those excludes match exactly what they exclude. Skips outside a workspace or when no router exists |
workspace-ci-router | error | The generated ci-router.yml exists at the repo root (it holds every project's inlined jobs; per-project coverage is workspace-ci-synced) |
workspace-ci-synced | error | Each in-scope project's CI jobs are inlined into the shared ci-router.yml. A member with no CI workflow file of its own (<member>/.github/workflows/ci*.yml) contributes no router jobs -- monorepo sync mints none for it -- so it is skipped with a note naming it rather than demanded. The root member is the usual case: its .github/workflows/ holds the generated routers, which are recognized as generated and never counted as its own workflows |
workspace-targets | error | Each project's declared target matches its actual manifest files |
workspace-unregistered | error | No project directories with manifest files exist outside of workspace.toml |
workspace-stale-entries | error | No workspace.toml entries point to directories that no longer exist |
dev-only-boundary | error | No non-dev-only project has a runtime dependency on a dev-only project |
unversioned-boundary | error | No releasable project has a runtime dependency on an unversioned project (releasable = false, not dev-only) |
dead-workspace-packages | warn | Detects workspace packages with no commits since their last release |
subtree-remote-reachable | error | Configured subtree remote URLs are reachable (requires network) |
mirror-required | error | Every member whose target is consumed by repository URL (SPM) belongs to a releasable that declares a subtree_remote; such a package is unresolvable from a monorepo without a standalone mirror |
workspace-unbuildable | error | Workspace members build under uv sync --all-packages (pypi workspaces only); also tagged preflight, so a manifest that stopped resolving blocks the release rather than only narrowing the router's derived filters |
scaffold-gitignore-stale | warn | Workspace project .gitignore files contain all rlsbl-managed entries |
root-rlsbl-conflict | error | Root .rlsbl/ does not coexist with .rlsbl-monorepo/ |
go-companion-tags | warn | Non-private Go members of releasables have companion tags for the current version; a broken member config is a hard failure |
releasable-residue | error | Release state sits where something will read it. A releasable member carries no per-package release state (.rlsbl/changes/, .rlsbl/releases/, .rlsbl/version, etc.) -- hooks/ and root-path members are exempt -- and a member that releases nothing (a dev node, or any member declared releasable = false) carries no release archives, changelog directory or version tags in its own scheme, unless a release-history-closed transition record event names it. The repository's transition record is read on every run, whatever the member list is, so a malformed transitions.jsonl reds the check even in a workspace where every member belongs to a releasable |
member-pytest-config | error | When the workspace root has a conftest.py, every member with a tests/ directory pins its own pytest rootdir, so a member run cannot escape into the root config |
mixed-tag-schemes | error | No member directory declares both Go's path-based {path}/v* tags and {name}@v* tags, which would make the publish-router prefix ordering-dependent |
test-suite-workspace (see prepush checks) is also tagged workspace.
#Quality checks
| Check | Severity | Description |
|---|---|---|
dead-modules | warn | Detects source modules with no inbound imports (unreachable code) |
dead-modules-stale | error | Every path declared in dead-modules.toml still exists, so an exclusion cannot silently outlive the file it excused |
circular-deps | warn | Detects circular import dependencies between modules |
library-lint | error | Runs lint rules for library projects (API surface, exports) |
ruff-lint | error | Project passes ruff lint checks (skipped when ruff is not installed) |
lint | error | Runs ruff check over the paths declared in the checks.lint config block; also tagged preflight. Skips when the config block is absent |
lint-scope-guard | error | ruff config carries no include/extend-include competing with checks.lint.paths; also tagged preflight |
format | error | Runs ruff format --check over the paths declared in the checks.format config block; also tagged preflight. Skips when the config block is absent |
format-scope-guard | error | ruff config carries no include/extend-include competing with checks.format.paths; also tagged preflight |
type-check | error | Runs mypy over the paths declared in the checks.type-check config block; also tagged preflight. Skips when the config block is absent |
type-check-scope-guard | error | mypy config carries no files/packages/modules competing with checks.type-check.paths; also tagged preflight |
deps-runtime-test-only | warn | Runtime dependencies that are only imported in test files |
deps-dev-in-lib | error | Dev dependencies used in library source (should be runtime deps) |
scaffold-unreplaced-vars | error | Leftover {{...}} template placeholders in workflow files |
maven-central-metadata | error | Maven Central publishing requirements are met (POM metadata, sources/javadoc jars); also tagged maven |
test-suite (see prepush checks) is also tagged quality.
#Prepush checks
| Check | Severity | Description |
|---|---|---|
prepush-changelog-coverage | error | Verifies every pushed commit has a JSONL changelog entry |
prepush-gitignore-guard | error | Blocks push if rlsbl-managed files are gitignored |
prepush-manual-warning | warn | Warns on manual push to release branch (non-blocking) |
test-suite | error | Runs project tests (pytest / go test / npm test) |
test-suite-workspace | error | Runs tests for affected workspace projects (monorepo only) |
scaffold-conflicts (see project checks) is also tagged prepush. Dependencies: test-suite and test-suite-workspace both depend on prepush-changelog-coverage -- fast checks fail first, so the test suite is skipped if changelog coverage fails. test-suite is also tagged quality, so it runs under both rlsbl check --tag prepush and rlsbl check --tag quality.
Both test-suite checks are overlay-preserving: when the project runs on rlsbl dev sync overlays, their uv sync excludes every overlaid package and the suite runs with uv run --no-sync. See the dev workflow for why a bare sync would otherwise wipe the overlays it is about to test.
#Untagged checks
These 4 checks have no tag assignment and run only when explicitly requested via --all or --name. They are excluded from tag-based runs because they require specific project configurations (layer rules, workspace manifests) or have longer execution times:
| Check | Severity | Description |
|---|---|---|
layers-violations | error | Dependency direction violates architectural layer rules defined in workspace.toml |
deps-unused | error | Declared dependencies that are never imported |
deps-undeclared | error | Imported packages that are not declared as dependencies |
deps-stale | error | Workspace dependency versions that are outdated relative to available versions |
#Framework checks
Every check above is declared in rlsbl's own checks.toml, and so are the counts on this page. The checks below are not: strictcli registers them into the same registry when rlsbl builds its CLI, so they run under rlsbl check --all and under the tags they carry, but no rlsbl-side count includes them. They carry the framework's own tags (test, effects), and the three effects lints also carry quality, so rlsbl check --tag quality runs them.
| Check | Severity | Tags | Description |
|---|---|---|---|
cli-test-coverage | error | test | Every registered command path appears in the committed coverage manifest (.strictcli/test-coverage.json), taken together with any per-process shard files under .strictcli/coverage/. Failure names each uncovered command. It skips when neither the manifest nor a shard exists -- an installed rlsbl running checks from someone else's project reports that instead of listing its whole command surface as uncovered |
effects-bypass | error | effects, quality | No process, filesystem-mutation or network call reachable from a registered command handler is made directly. Each finding names the file, the line and the function being called, and the remedy is always the same: route it through ctx.effects |
observe-allowlist-breadth | warn | effects, quality | No proc_observe_allowlist prefix is a single token. A one-token prefix makes every invocation of that binary an observe, so it really executes under --dry-run, is never written to the would-do log, and is legal inside a read_only command. A warning, because the allowlist is a declared, source-visible choice |
consequential-grant-agreement | warn | effects, quality | Every command declaring a grant whose kind leaves this process (proc_mutate runs another program, net_mutate changes remote state) also declares itself consequential. A warning, because the two declarations can legitimately disagree -- making it an error would push consumers to declare consequential reflexively |
#Target applicability
Not every check applies to every release target. Each check declares its applicability as one of three categories, which determines whether it runs for a given project based on the project's detected targets:
- Universal (
None): runs for any target -- most project, release, and changelog checks - Workspace-only (
"workspace"): runs only in monorepo workspaces, target-agnostic - Target-specific (
frozenset): requires specific language targets with import scanners or AST analysis
| Check | pypi | go | npm | dart | hex | swift | swift-apple | maven | flutter |
|---|---|---|---|---|---|---|---|---|---|
| ci-publish-secrets | no | no | yes | no | yes | no | no | yes | no |
| circular-deps | yes | n/a | yes | yes | no | no | no | yes | yes |
| cross-repo-path-sources | yes | no | no | no | no | no | no | no | no |
| dead-modules | yes | yes | yes | yes | no | no | no | yes | yes |
| dead-modules-stale | yes | yes | yes | yes | no | no | no | yes | yes |
| dep-floors | yes | yes | yes | no | no | no | no | no | no |
| dep-locks | yes | yes | yes | no | no | no | no | no | no |
| deps-dev-in-lib | yes | yes | yes | yes | no | no | no | yes | yes |
| deps-runtime-test-only | yes | yes | yes | yes | no | no | no | yes | yes |
| deps-undeclared | yes | yes | yes | yes | no | no | no | yes | yes |
| deps-unused | yes | yes | yes | yes | no | no | no | yes | yes |
| dev-overlay-drift | yes | no | no | no | no | no | no | no | no |
| dunder-version-missing | yes | no | no | no | no | no | no | no | no |
| format | yes | no | no | no | no | no | no | no | no |
| format-scope-guard | yes | no | no | no | no | no | no | no | no |
| go-module-identity | no | yes | no | no | no | no | no | no | no |
| ldflags-symbol | no | yes | no | no | no | no | no | no | no |
| library-lint | yes | yes | yes | no | no | no | no | yes | no |
| lint | yes | no | no | no | no | no | no | no | no |
| lint-scope-guard | yes | no | no | no | no | no | no | no | no |
| maven-central-metadata | no | no | no | no | no | no | no | yes | no |
| mirror-required | no | no | no | no | no | yes | yes | no | no |
| npm-private-mismatch | no | no | yes | no | no | no | no | no | no |
| ruff-lint | yes | no | no | no | no | no | no | no | no |
| strictspec-generated-floor | yes | no | no | no | no | no | no | no | no |
| test-suite | yes | yes | yes | no | no | no | no | yes | no |
| type-check | yes | no | no | no | no | no | no | no | no |
| type-check-scope-guard | yes | no | no | no | no | no | no | no | no |
#Excluded targets
Some checks explicitly exclude specific targets where the compiler or language toolchain already enforces the same constraint natively, making rlsbl's check redundant. These exclusions prevent false positives and unnecessary warnings:
| Check | Excluded target | Reason |
|---|---|---|
circular-deps | go | Go compiler rejects circular imports |
#Check metadata
Checks are declared in rlsbl/data/checks.toml with metadata that controls execution order, dependency resolution, and result severity. Each check entry has the following fields that the check runner uses to determine when and how to execute the check:
| Field | Type | Description |
|---|---|---|
tags | array of strings | Which tags include this check (empty = untagged, only runs with --all or --name) |
severity | "error" or "warn" | Whether failure blocks (fail) or advises (warn) |
fast | bool | Whether the check completes quickly (used for prioritization) |
pure | bool | Whether the check starts only programs on the observe allowlist (see Purity below) |
needs_network | bool | Whether the check requires network access (e.g., GitHub API calls) |
depends_on | array of strings | Other checks that must pass first (skipped if dependency fails) |
Checks are implemented via the @app.error_check("<name>") and @app.warn_check("<name>") decorators in the rlsbl/checks/ package (one module per tag, e.g. project.py, release.py, workspace.py), which register the function with strictcli's check system. The name passed to the decorator must match the key in checks.toml, and the decorator chosen must match that entry's severity.
#Purity
A pure check starts only read-only programs on the observe allowlist. The allowlist is rlsbl/observe_allowlist.py, whose written standard is no user-visible mutation: ref updates, index writes and credential emission are refused there, so any program that reaches the list changes nothing a user would notice. A check that starts no program at all is trivially pure.
A check is impure when it starts a program that is not on that list. Every impure check today runs a tool that writes: ruff rewrites files, uv sync materializes an environment, the test suites and gradle build.
Purity decides what a preview does. Under rlsbl release run --dry-run the preflight runs its pure checks for real and lists the impure ones as would run: <name> (impure) -- so a preview reports real findings from everything that can be run without changing anything, and is honest about the rest.
This rule replaced an older one, "the check starts no program at all". That rule forced nine checks that spawn only read-only local git (the changelog validators, the two pre-push checks, workspace-unregistered, go-companion-tags) to be declared impure, and it quietly declared two checks pure that do spawn: the retired local-tag ran git tag --list, and config-schema can reach go list on its error path. All of them are pure under the current rule, and are now declared so deliberately rather than by accident.
The declaration is verified, not trusted: tests/test_check_purity.py executes every pure-declared check under an effects observer and fails on any spawn whose argv matches no allowlist prefix.
needs_network is orthogonal: it says whether a check needs the network to answer at all, never whether it may mutate. A pure check may be a network read.
#Examples
#Running all checks before a release
rlsbl check --all
# lock .......................... pass
# version-consistency ........... pass
# config-schema ................. pass
# license-file .................. pass
# scaffold-conflicts ............ pass
# cross-repo-path-sources ....... pass
# changelog-hashes .............. pass
# changelog-range ............... pass
# changelog-coverage ............ FAIL
# Uncovered commits:
# a1b2c3d Add retry logic
# e4f5g6h Fix timeout bug
# changelog-schema .............. pass
# changelog-user-facing ......... warn No user-facing entries
# unpublished-refs ............. fail v0.5.2: the ref v0.5.2 exists locally but not on origin
# test-suite ................... pass
#
# 12 passed, 1 failed, 2 warnings#Investigating a specific check failure
# Run just the failing check to see detailed output
rlsbl check --name changelog-coverage
# changelog-coverage ............ FAIL
# Uncovered commits:
# a1b2c3d Add retry logic
# e4f5g6h Fix timeout bug
# Fix: run `rlsbl changelog add --commits <hash> ...` for each
# Fix it
rlsbl changelog add --commits a1b2c3d --description "Add retry logic to HTTP client" --type feature
rlsbl changelog add --commits e4f5g6h --description "Fix timeout crash on slow connections" --type fix
# Verify the fix
rlsbl check --name changelog-coverage
# changelog-coverage ............ pass#Checking workspace integrity in a monorepo
rlsbl check --tag workspace
# workspace-ci-router ........... pass
# workspace-ci-synced ........... pass
# workspace-targets ............. pass
# workspace-unregistered ........ FAIL
# packages/new-lib/ has pyproject.toml but is not in workspace.toml
# workspace-stale-entries ....... pass
# dev-only-boundary ............. pass
# dead-workspace-packages ....... warn library 'old-utils' not imported by any workspace package
#
# Fix: run `rlsbl monorepo add packages/new-lib --target pypi --releasable new-lib`#Pre-push check output
# Triggered automatically by git push, or run manually:
rlsbl check --tag prepush
# prepush-changelog-coverage .... pass
# prepush-gitignore-guard ....... pass
# prepush-manual-warning ........ skip (not a release branch push)
# test-suite .................... pass
# scaffold-conflicts ............ pass