rlsbl v0.92.0 /rlsbl.changelog.validate
On this page

Validates JSONL changelog entries against git history: hash resolution, range checking, commit coverage with monorepo filtering, orphans, and schema.

#rlsbl.changelog.validate

#rlsbl.changelog.validate

Validates JSONL changelog entries against git history: hash resolution, range checking, commit coverage, orphan detection, and schema conformance.

#_filter_commits_for_scope

python
def _filter_commits_for_scope(commits, project)

Filter commits using the right strategy for the project scope.

When project is a list (releasable members in explicit mode), delegates to filter_commits_for_releasable. When it is a single project dict, delegates to filter_commits_for_project. Returns the input unchanged when project is None (standalone mode).

#_get_batch_limits_config

python
def _get_batch_limits_config(config) -> dict

Return the resolved batch_limits config with defaults applied.

config is the project config dict (already loaded). Reads the raw batch_limits section via :func:get_changelog_validation_config and guarantees that all three expected keys are present with sane types:

  • max_commits_per_entry (int)
  • max_entries_per_commit (int)
  • exclusions (list)

If any key is missing or has the wrong type, the default is used and a warning is emitted on stderr.

#_git_log_hashes

python
def _git_log_hashes(range_spec: str) -> list[str]

Get commit hashes from git log for a given range spec.

Returns a list of full 40-char SHAs, or empty list on error.

#_get_last_version_tag

python
def _get_last_version_tag(tag_glob: str | None=None) -> str | None

Backward-compatible wrapper around get_last_version_tag from utils.

Accepts None for tag_glob (defaulting to "v*") to preserve the old call signature used by _unreleased_range and external callers like status.py.

#_unreleased_range

python
def _unreleased_range(tag_glob: str | None=None) -> str

Return the git log range spec for unreleased commits.

Uses ..HEAD if a version tag exists, otherwise HEAD (all commits, for first release). Passes tag_glob through to _get_last_version_tag for monorepo-aware tag discovery.

#_git_head

python
def _git_head() -> str | None

Get the current HEAD commit hash.

#filter_exempt_commits

python
def filter_exempt_commits(commits: list[str]) -> tuple[list[str], ExemptionStats]

Filter out commits exempt from changelog coverage.

Checks each commit against two predicates in canonical order:

  1. is_autogenerated (commit has Autogenerated: true trailer)
  2. is_changelog_only_commit (commit only touches changelog files)

First predicate match wins for stats counting: a commit that is both autogenerated and changelog-only is counted as autogenerated only.

Returns a tuple of (non-exempt commits, stats with per-predicate counts).

#_is_ancestor

python
def _is_ancestor(ancestor: str, descendant: str) -> bool

Check if ancestor is an ancestor of descendant.

#_cache_path

python
def _cache_path(changes_dir: str) -> str

Return path to the .validated cache file.

#_read_cache

python
def _read_cache(changes_dir: str) -> str | None

Read the .validated file. Return the cached HEAD hash or None.

#_write_cache

python
def _write_cache(changes_dir: str) -> None

Write the current HEAD hash to the .validated cache file.

#_is_cache_valid

python
def _is_cache_valid(changes_dir: str) -> bool

Check if the validation cache is still valid.

Valid when:

  • .validated exists and contains a 40-char SHA
  • That SHA is an ancestor of (or equal to) HEAD
  • unreleased.jsonl's mtime is older than .validated's mtime

#check_hashes_resolve

python
def check_hashes_resolve(entries: list[ChangelogEntry]) -> tuple[bool, list[str]]

Check that every hash in every entry resolves via git rev-parse.

#check_in_range

python
def check_in_range(entries: list[ChangelogEntry], tag_glob: str | None=None, project=None) -> tuple[bool, list[str]]

Check that every resolved hash is in the unreleased range.

Unreleased range: commits since the last version tag (or all commits if no tags exist). When tag_glob is set, scopes to monorepo tags. When project is set (monorepo mode), only commits touching the project's files are considered in-range. project may be a single project dict or a list of projects (releasable members).

#check_coverage

python
def check_coverage(entries: list[ChangelogEntry], tag_glob: str | None=None, project=None) -> tuple[bool, list[str]]

Check that every unreleased commit appears in at least one entry.

Commits with the Autogenerated: true trailer are automatically exempted -- they are release infrastructure and don't need coverage. When tag_glob is set, scopes to monorepo tags. When project is set (monorepo mode), only commits touching the project's files require coverage -- commits outside the package directory are filtered out. project may be a single project dict or a list of projects (releasable members).

#check_no_orphans

python
def check_no_orphans(entries: list[ChangelogEntry], tag_glob: str | None=None, project=None) -> tuple[bool, list[str]]

Flag entries where all commits are stale (unresolvable or out of range).

An entry is fully orphaned when ALL its hashes are unresolvable. An entry is effectively orphaned when every hash is either unresolvable or outside the unreleased range — no commit is both valid and in range. project may be a single project dict or a list of projects (releasable members).

#check_schema

python
def check_schema(entries: list[ChangelogEntry]) -> tuple[bool, list[str]]

Check that every entry passes schema validation.

#check_has_user_facing

python
def check_has_user_facing(entries: list[ChangelogEntry]) -> tuple[bool, list[str]]

Check that at least one entry is user-facing.

#check_batch_size_commits

python
def check_batch_size_commits(entries: list[ChangelogEntry], config: dict, version: str='unreleased') -> tuple[bool, list[str]]

Check that no entry has more commits than max_commits_per_entry.

config is the resolved batch_limits config (see :func:_get_batch_limits_config). Per-entry exclusions (matched by version + 1-based line number) silence the check for those entries.

#check_batch_size_entries

python
def check_batch_size_entries(entries_by_version: dict[str, list[ChangelogEntry]], config: dict) -> tuple[bool, list[str]]

Check that no commit appears in more than max_entries_per_commit entries.

entries_by_version maps version label (e.g. "unreleased" or "0.32.0") to its entry list, so the check spans across ALL JSONL files, not just unreleased. Per-commit exclusions in config silence specific hashes.

#_read_all_versioned_entries

python
def _read_all_versioned_entries(changes_dir: str) -> dict[str, list[ChangelogEntry]]

Read entries from unreleased.jsonl AND every x.y.z.jsonl in changes_dir.

Returns a mapping {version_label: entries} where version_label is "unreleased" or the bare semver string (e.g. "0.32.0"). Files that fail to parse are skipped silently -- other checks surface such errors separately.

#validate_unreleased

python
def validate_unreleased(changes_dir: str, tag_glob: str | None=None, project=None, *, config: dict, bump_type: str | None=None) -> dict

Run all 8 validation checks on unreleased.jsonl.

Returns a dict with:

  • check names as keys, (passed, details) tuples as values
  • "passed": overall bool (True only if all checks pass)

Uses validation cache: if the cache is valid and HEAD hasn't changed, skips full revalidation. When tag_glob is set, uses it directly as the glob pattern for monorepo tag discovery (e.g. mylib@v* or go/v*). When project is set (monorepo mode), coverage and range checks only consider commits touching the project's files. project may be a single project dict or a list of projects (releasable members).

The two batch_limits checks (batch_size_commits and batch_size_entries) read configuration via :func:_get_batch_limits_config from the provided config dict. The cross-version batch_size_entries check reads every x.y.z.jsonl file in changes_dir so it can detect commits that appear in too many entries across versions.