On this page
Validates JSONL changelog entries against git history: hash resolution, range checking, commit coverage, orphan detection, and schema conformance.
#rlsbl.changelog.validate
#rlsbl.changelog.validate
Validates JSONL changelog entries against git history: hash resolution, range checking, commit coverage, orphan detection, and schema conformance.
#_get_batch_limits_config
def _get_batch_limits_config() -> dictReturn the resolved batch_limits config with defaults applied.
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 | NoneGet the most recent version tag (e.g., v0.25.2).
When tag_glob is set (monorepo mode), uses it directly as the --match pattern (e.g. mylib@v* or go/v*).
Returns the tag string or None if no version tags exist.
#_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.
#_is_changelog_only_commit
def _is_changelog_only_commit(sha: str) -> boolCheck if a commit only touches changelog/release infrastructure files.
Returns True when every file changed by the commit matches one of:
.rlsbl/changes/(any depth).rlsbl/versionCHANGELOG.md
For monorepo support, paths prefixed with a subdirectory are also accepted (e.g. python/.rlsbl/changes/unreleased.jsonl).
Subprocess errors or empty file lists are treated conservatively (returns False).
#_is_changelog_path
def _is_changelog_path(path: str) -> boolReturn True if path is a changelog/release infrastructure file.
Recognised patterns (with optional leading directory prefix):
[prefix/].rlsbl/changes/...[prefix/].rlsbl/version[prefix/]CHANGELOG.md
#_is_autogenerated
def _is_autogenerated(sha: str) -> boolCheck if a commit has the Autogenerated: true trailer.
Returns True when the trailer is present with value true, False otherwise. Subprocess errors are treated as non-autogenerated.
#_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) -> 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.
#check_coverage
def check_coverage(entries: list[ChangelogEntry], tag_glob: str | None=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.
#check_no_orphans
def check_no_orphans(entries: list[ChangelogEntry]) -> tuple[bool, list[str]]Flag entries where ALL hashes are unresolvable (stale/rebased entries).
#check_schema
def check_schema(entries: list[ChangelogEntry]) -> tuple[bool, list[str]]Check that every entry passes schema validation.
#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) -> dictRun all 7 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*).
The two batch_limits checks (batch_size_commits and batch_size_entries) read configuration from .rlsbl/config.json via :func:_get_batch_limits_config. 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.