Skip to content
Check system
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

$_ bash
# 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:

Check results
StatusMeaningEffect
passCheck passedNo action needed
failCheck failedBlocking error -- must be fixed before release
warnAdvisory findingInformational -- does not block release
skipNot applicableCheck 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

Tags
TagPurposeCheck count
projectProject-level metadata, config schema, version consistency27
releaseReleased-version refs, branch sync, CI credentials, and conversion follow-ups6
changelogJSONL changelog validation and structure11
workspaceMonorepo workspace integrity and dependency rules19
qualityCode quality, dependency analysis, scaffold hygiene16
prepushPre-push enforcement: changelog coverage, gitignore guard, manual-push warning, tests6

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

Project checks
CheckSeverityDescription
lockwarnDetects stale lock state in .rlsbl/
version-consistencyerrorProject version matches across all target files (e.g., pyproject.toml, package.json, .rlsbl/version)
name-consistencywarnPackage name is consistent across manifest files
description-consistencywarnPackage description is consistent across manifest files
license-fileerrorA LICENSE file exists in the project root
license-consistencywarnLicense identifier matches across manifest files
config-schemaerrorThe 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-staleerrorDetects leftover private repo hook files that should be deleted
publish-mode-workflowerrorpublish_mode: "none" repos must not have a publish workflow that pushes to public registries
npm-private-mismatcherrorpackage.json private field matches .rlsbl/config.json private flag (npm targets only)
target-version-readableerrorVersion can be read from all declared target files
dunder-version-missingerrorPyPI targets that keep a version constant in source must use __version__
selfdoc-version-drifterrorselfdoc-generated version references match the actual project version
scaffold-conflictserrorUnresolved git merge conflict markers in scaffold files (managed-files registry, .github/workflows/, all of .rlsbl/); also tagged prepush and release
stash-freeerrorThe 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-sourceserror[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-drifterrorPackages 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-serviceserrorCI service containers declared under services/test_env are actually provisioned in the rendered CI workflow
wrapper-producererrorEvery 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-gateerrorA configured strictspec diff certificate reports no violated (or unsupported-and-unadjudicated) claim. Skips when the project has no strictspec_gate section
stricttest-floorerrorAn 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-floorserrorEcosystem-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-lockserrorEvery 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-identityerrorEvery 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-symbolerrorEvery -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-floorerrorThe 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-fresherrorThe 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

Release checks
CheckSeverityDescription
unpublished-refserrorEvery 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-syncerrorLocal branch is not behind the remote tracking branch (requires network)
ci-publish-secretserrorEvery 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-archivederrorEvery 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-publishederrorEvery 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:

unpublished-refs
FailureWhat it means
missing locallyThe release record records the version as released, but the ref is not in this repository
missing on originThe ref exists locally but was never pushed, so consumers cannot resolve it
wrong commitThe ref exists but points somewhere other than the release's release commit, so it moved after the release wrote it
no GitHub ReleaseThe 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

Changelog checks
CheckSeverityDescription
changelog-hasheserrorEvery commit hash in JSONL entries resolves via git rev-parse
changelog-rangeerrorEvery 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-coverageerrorEvery unreleased commit appears in at least one JSONL entry
changelog-orphanserrorNo entries whose every hash is unresolvable, out of range, or owned by another releasable (stale from rebased/amended commits, or cross-filed)
changelog-schemaerrorUser-facing entries have description and type; type is one of feature/fix/breaking
changelog-user-facingwarnAt least one entry is user-facing (hard error during release, warning in check mode)
changelog-batch-commitserrorNo single entry references more commits than max_commits_per_entry (default 5)
changelog-batch-entrieserrorNo single commit appears in more entries than max_entries_per_commit (default 5)
changelog-entrywarnCHANGELOG.md contains an entry for the current project version
changelog-format-versionwarnThe repo has recorded a changelog_format_version_enforced decision (enabling the gate below, or staying in legacy mode deliberately)
changelog-format-version-gateerrorWhen 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

Workspace checks
CheckSeverityDescription
router-filters-fresherrorThe 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-routererrorThe 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-syncederrorEach 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-targetserrorEach project's declared target matches its actual manifest files
workspace-unregisterederrorNo project directories with manifest files exist outside of workspace.toml
workspace-stale-entrieserrorNo workspace.toml entries point to directories that no longer exist
dev-only-boundaryerrorNo non-dev-only project has a runtime dependency on a dev-only project
unversioned-boundaryerrorNo releasable project has a runtime dependency on an unversioned project (releasable = false, not dev-only)
dead-workspace-packageswarnDetects workspace packages with no commits since their last release
subtree-remote-reachableerrorConfigured subtree remote URLs are reachable (requires network)
mirror-requirederrorEvery 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-unbuildableerrorWorkspace 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-stalewarnWorkspace project .gitignore files contain all rlsbl-managed entries
root-rlsbl-conflicterrorRoot .rlsbl/ does not coexist with .rlsbl-monorepo/
go-companion-tagswarnNon-private Go members of releasables have companion tags for the current version; a broken member config is a hard failure
releasable-residueerrorRelease 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-configerrorWhen 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-schemeserrorNo 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

Quality checks
CheckSeverityDescription
dead-moduleswarnDetects source modules with no inbound imports (unreachable code)
dead-modules-staleerrorEvery path declared in dead-modules.toml still exists, so an exclusion cannot silently outlive the file it excused
circular-depswarnDetects circular import dependencies between modules
library-linterrorRuns lint rules for library projects (API surface, exports)
ruff-linterrorProject passes ruff lint checks (skipped when ruff is not installed)
linterrorRuns ruff check over the paths declared in the checks.lint config block; also tagged preflight. Skips when the config block is absent
lint-scope-guarderrorruff config carries no include/extend-include competing with checks.lint.paths; also tagged preflight
formaterrorRuns 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-guarderrorruff config carries no include/extend-include competing with checks.format.paths; also tagged preflight
type-checkerrorRuns 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-guarderrormypy config carries no files/packages/modules competing with checks.type-check.paths; also tagged preflight
deps-runtime-test-onlywarnRuntime dependencies that are only imported in test files
deps-dev-in-liberrorDev dependencies used in library source (should be runtime deps)
scaffold-unreplaced-varserrorLeftover {{...}} template placeholders in workflow files
maven-central-metadataerrorMaven Central publishing requirements are met (POM metadata, sources/javadoc jars); also tagged maven

test-suite (see prepush checks) is also tagged quality.

#Prepush checks

Prepush checks
CheckSeverityDescription
prepush-changelog-coverageerrorVerifies every pushed commit has a JSONL changelog entry
prepush-gitignore-guarderrorBlocks push if rlsbl-managed files are gitignored
prepush-manual-warningwarnWarns on manual push to release branch (non-blocking)
test-suiteerrorRuns project tests (pytest / go test / npm test)
test-suite-workspaceerrorRuns 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:

Untagged checks
CheckSeverityDescription
layers-violationserrorDependency direction violates architectural layer rules defined in workspace.toml
deps-unusederrorDeclared dependencies that are never imported
deps-undeclarederrorImported packages that are not declared as dependencies
deps-staleerrorWorkspace 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.

Framework checks
CheckSeverityTagsDescription
cli-test-coverageerrortestEvery 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-bypasserroreffects, qualityNo 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-breadthwarneffects, qualityNo 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-agreementwarneffects, qualityEvery 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
Target applicability
Checkpypigonpmdarthexswiftswift-applemavenflutter
ci-publish-secretsnonoyesnoyesnonoyesno
circular-depsyesn/ayesyesnononoyesyes
cross-repo-path-sourcesyesnononononononono
dead-modulesyesyesyesyesnononoyesyes
dead-modules-staleyesyesyesyesnononoyesyes
dep-floorsyesyesyesnononononono
dep-locksyesyesyesnononononono
deps-dev-in-libyesyesyesyesnononoyesyes
deps-runtime-test-onlyyesyesyesyesnononoyesyes
deps-undeclaredyesyesyesyesnononoyesyes
deps-unusedyesyesyesyesnononoyesyes
dev-overlay-driftyesnononononononono
dunder-version-missingyesnononononononono
formatyesnononononononono
format-scope-guardyesnononononononono
go-module-identitynoyesnonononononono
ldflags-symbolnoyesnonononononono
library-lintyesyesyesnonononoyesno
lintyesnononononononono
lint-scope-guardyesnononononononono
maven-central-metadatanononononononoyesno
mirror-requirednononononoyesyesnono
npm-private-mismatchnonoyesnononononono
ruff-lintyesnononononononono
strictspec-generated-flooryesnononononononono
test-suiteyesyesyesnonononoyesno
type-checkyesnononononononono
type-check-scope-guardyesnononononononono

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

Excluded targets
CheckExcluded targetReason
circular-depsgoGo 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:

Check metadata
FieldTypeDescription
tagsarray of stringsWhich tags include this check (empty = untagged, only runs with --all or --name)
severity"error" or "warn"Whether failure blocks (fail) or advises (warn)
fastboolWhether the check completes quickly (used for prioritization)
pureboolWhether the check starts only programs on the observe allowlist (see Purity below)
needs_networkboolWhether the check requires network access (e.g., GitHub API calls)
depends_onarray of stringsOther 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

$_ bash
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

$_ bash
# 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

$_ bash
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

$_ bash
# 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

More tools from this site

  • claudestream Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python
  • claudewheel A TUI Claude Code Launcher that lets you have more than one profile, manage sessions lifecycle, pick the exact CC version, model to use (even older unlisted ones), pick which GitHub account to use, etc.
  • dirstat Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON
  • fastware A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client
  • go-toml-edit Zero-dep TOML editing library for Go with comment preservation
  • howmuchleft The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms
  • orxtra
  • pgdesign
  • predraw Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines
  • reposummary Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM
  • safegit git wrapper CLI that gives each commit its own temporary index and retries ref updates on conflict, so concurrent agents share one repository
  • saferm Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored
  • selfdoc Static Site Generator that builds a project's documentation site directly from its source code, so the docs can never drift from the code they describe, with SEO/AEO, first-class blog, search, and cross-project linking built in
  • strictcli
  • stricttest An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.
  • wesktop A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview
Search