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
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
def _get_batch_limits_config(config) -> dictReturn 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
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
def _get_last_version_tag(tag_glob: str | None=None) -> str | NoneBackward-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
def _unreleased_range(tag_glob: str | None=None) -> strReturn the git log range spec for unreleased commits.
Uses
#_git_head
def _git_head() -> str | NoneGet the current HEAD commit hash.
#filter_exempt_commits
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:
is_autogenerated(commit hasAutogenerated: truetrailer)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
def _is_ancestor(ancestor: str, descendant: str) -> boolCheck if ancestor is an ancestor of descendant.
#_cache_path
def _cache_path(changes_dir: str) -> strReturn path to the .validated cache file.
#_read_cache
def _read_cache(changes_dir: str) -> str | NoneRead the .validated file. Return the cached HEAD hash or None.
#_write_cache
def _write_cache(changes_dir: str) -> NoneWrite the current HEAD hash to the .validated cache file.
#_is_cache_valid
def _is_cache_valid(changes_dir: str) -> boolCheck 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
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
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
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
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
def check_schema(entries: list[ChangelogEntry]) -> tuple[bool, list[str]]Check that every entry passes schema validation.
#check_has_user_facing
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
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
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
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
def validate_unreleased(changes_dir: str, tag_glob: str | None=None, project=None, *, config: dict, bump_type: str | None=None) -> dictRun 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.