rlsbl v0.92.0 /rlsbl.commands.release.hooks
On this page

Hook helpers: content hashing, template hash lookup, hook emptiness check, hook runner.

#rlsbl.commands.release.hooks

#rlsbl.commands.release.hooks

Hook helpers: content hashing, template hash lookup, hook emptiness check, hook runner.

Supports two levels of hooks in monorepo explicit mode:

  • Per-releasable: .rlsbl-monorepo/releasables/{name}/hooks/
  • Per-package: .rlsbl/hooks/ within each member package

Hook sources (checked in order):

  1. Config-driven: hooks key in project/releasable config.json
  2. Script-based: shell scripts in .rlsbl/hooks/ directories

Config-driven hooks use dual syntax (each entry is a string or structured object):

  • String shorthand: "npm test" is equivalent to {"cmd": "npm test"}
  • Structured: {"cmd": "npm test", "dir": "./submodule", "env": {"NODE_ENV": "test"}}

Execution order during release:

  1. Releasable pre-checks
  2. Per-package pre-checks (alphabetical by package name)
  3. Built-in tests (each member)
  4. Per-package pre-release (alphabetical)
  5. Releasable pre-release

#_compute_content_hash

python
def _compute_content_hash(content)

SHA-256 of content with trailing whitespace stripped.

#_get_pre_release_template_hashes

python
def _get_pre_release_template_hashes()

Return a frozenset of content hashes for known scaffold template versions of pre-release.sh.

Used by the backward compatibility bridge to detect whether an existing script-based hook has been customized or is still the default template.

#_is_hook_effectively_empty

python
def _is_hook_effectively_empty(hook_path)

Check if a pre-release hook file is effectively empty (matches scaffold template).

Returns True (hook is boilerplate / not customized) when:

  • The hook file does not exist
  • The hook file's content hash matches a known scaffold template version

Returns False (hook has been customized) when:

  • The hook exists and its content does not match any known template version

#warn_if_hook_needs_migration

python
def warn_if_hook_needs_migration(config, hook_path)

Emit a warning if a customized pre-release.sh exists without config entries.

This detects projects that still use the old script-based hook system without having migrated to config-driven hooks. The warning is informational only -- no auto-conversion is performed.

Args:

  • config: project config dict (from ctx.config).
  • hook_path: absolute path to the pre-release.sh script.

Returns:

  • True if a warning was emitted, False otherwise.

#normalize_hook_entry

python
def normalize_hook_entry(entry)

Convert a hook entry to structured dict form.

String entries are shorthand for {"cmd": }. Dict entries are validated for required cmd field.

Args:

  • entry: a string or dict hook entry.

Returns:

  • A dict with at least cmd (str), and optionally dir (str)
  • and env (dict).

Raises:

  • HookError: if entry is neither str nor dict, or if dict is missing cmd.

#_get_config_hooks

python
def _get_config_hooks(hook_name, config)

Read hook entries from config for a given hook slot.

Args:

  • hook_name: hyphenated hook name (e.g. "pre-checks").
  • config: project config dict.

Returns:

  • List of hook entries (strings or dicts), or None if the config
  • has no entries for this hook slot.

Raises:

  • HookError: if the hook slot value is not a list.

#run_config_hooks

python
def run_config_hooks(hook_name, config, project_dir, env, timeout, *, fatal=True, display_name=None)

Run config-driven hooks for a given hook slot.

Reads config["hooks"][], normalizes each entry, and runs them in order via subprocess.

Args:

  • hook_name: canonical hyphenated hook name for config lookup

(e.g. "pre-checks").

  • config: project config dict containing a hooks key.
  • project_dir: base working directory.
  • env: environment dict to pass to subprocesses.
  • timeout: seconds before a command is killed.
  • fatal: if True (default), non-zero exit raises HookError.

If False, logs a warning and continues (for post-release).

  • display_name: human-readable name for error messages. If None,

uses hook_name.

Returns:

  • True if config had entries for this hook (even if empty list),
  • False if config had no entries (caller should fall back to scripts).

Raises:

  • HookError: on non-zero exit or timeout (when fatal=True).

#is_hook_customized

python
def is_hook_customized(config, hook_path)

Check if the pre-release hook is customized, using config-first detection.

Decision logic:

  1. If config has a non-empty hooks.pre_release commands list, the hook

is considered customized (built-in tests and lint are skipped).

  1. If config has hooks.pre_release as an empty list, the hook is NOT

customized (built-in tests and lint run).

  1. If config has no hooks section or no pre_release key, fall back

to the script-file hash check (backward compat during migration): - Customized script file -> customized - Template/missing script file -> not customized

Args:

  • config: project config dict (from ctx.config).
  • hook_path: absolute path to the pre-release.sh script.

Returns:

  • True if the hook is customized (skip built-in tests/lint),
  • False otherwise.

#run_release_hook

python
def run_release_hook(hook_name, hook_path, project_dir, env, timeout, *, config=None)

Run a release hook: config-driven if available, else script-based.

When config is provided and has entries for the hook slot, runs config-driven hooks and ignores the script file. Otherwise falls back to the script at hook_path.

hook_name: human-readable name for error messages (e.g. "pre-checks", "releasable pre-checks", "pre-checks (mypkg)"). May include a prefix for display purposes; the canonical hook name for config lookup is extracted automatically. hook_path: absolute path to the shell script (fallback). project_dir: working directory for the hook. env: environment dict to pass to the subprocess. timeout: seconds before the hook is killed. config: project config dict (optional). If provided, checked for config-driven hook entries before falling back to script.

Raises HookError on non-zero exit or timeout.

#get_releasable_hook_path

python
def get_releasable_hook_path(workspace_root, releasable_name, hook_name)

Return the absolute path to a releasable-level hook script.

Path: /.rlsbl-monorepo/releasables//hooks/

Args:

  • workspace_root: path to the monorepo root.
  • releasable_name: name of the releasable.
  • hook_name: hook file name (e.g. "pre-checks.sh").

Returns:

  • Absolute path string. The file may or may not exist on disk.

#get_package_hook_path

python
def get_package_hook_path(package_dir, hook_name)

Return the absolute path to a per-package hook script.

Path: /.rlsbl/hooks/

Args:

  • package_dir: absolute path to the package directory.
  • hook_name: hook file name (e.g. "pre-checks.sh").

Returns:

  • Absolute path string. The file may or may not exist on disk.

#build_hook_env

python
def build_hook_env(base_env, version, *, package_name=None, bump_type=None, prev_version=None, description=None)

Build the environment dict for hook execution.

Always sets RLSBL_VERSION. When package_name is provided (per-package hooks), also sets RLSBL_PACKAGE.

Args:

  • base_env: base environment dict (typically os.environ.copy()).
  • version: the new release version string.
  • package_name: current package name (for per-package hooks), or None.
  • bump_type: bump type string (patch/minor/major), or None.
  • prev_version: previous version string, or None.
  • description: release description string, or None.

Returns:

  • A new dict with hook-specific env vars added.

#run_releasable_hooks

python
def run_releasable_hooks(hook_name, workspace_root, releasable_name, member_packages, hook_env, timeout, log, *, project_dir=None, releasable_config=None, package_configs=None)

Run hooks at both releasable and per-package levels.

For pre-checks: releasable first, then per-package (alphabetical). For pre-release: per-package first (alphabetical), then releasable. For post-release: releasable first, then per-package (alphabetical).

Each per-package hook gets RLSBL_PACKAGE added to its env.

Config-driven hooks take precedence over script files at each level independently. The releasable level uses releasable_config and per-package level uses package_configs. If config has entries for a hook slot, scripts are ignored for that level.

Args:

  • hook_name: hook file name without extension context, e.g. "pre-checks".
  • workspace_root: path to the monorepo root.
  • releasable_name: name of the releasable.
  • member_packages: list of (package_name, package_dir) tuples, sorted

alphabetically by name.

  • hook_env: base environment dict (without RLSBL_PACKAGE).
  • timeout: seconds before a hook is killed.
  • log: callable for logging messages.
  • project_dir: fallback cwd for releasable-level hooks. If None,

uses workspace_root.

  • releasable_config: releasable-level config dict (optional).
  • package_configs: dict mapping package_name to per-package config dict

(optional).

Raises:

  • HookError on first failure at any level.

#_run_per_package_hooks

python
def _run_per_package_hooks(hook_name, hook_file, sorted_members, hook_env, timeout, log, *, package_configs=None)

Run a hook for each member package that has it, in alphabetical order.

Adds RLSBL_PACKAGE to the env for each package. Config-driven hooks take precedence over script files per-package.

Args:

  • hook_name: human-readable hook name (e.g. "pre-checks").
  • hook_file: hook file name (e.g. "pre-checks.sh").
  • sorted_members: list of (package_name, package_dir) tuples, already sorted.
  • hook_env: base environment dict (without RLSBL_PACKAGE).
  • timeout: seconds before a hook is killed.
  • log: callable for logging messages.
  • package_configs: dict mapping package_name to per-package config dict

(optional).

Raises:

  • HookError on first failure.

#is_releasable_hook_customized

python
def is_releasable_hook_customized(workspace_root, releasable_name, config=None)

Check if a releasable's pre-release hook is customized (not scaffold boilerplate).

When config is provided, checks config-driven hooks first:

  1. If config has a non-empty hooks.pre_release list -> customized
  2. If config has an empty hooks.pre_release list -> not customized
  3. If config has no hooks section -> fall back to script hash check

The "effectively empty" check is at the releasable level: if the releasable's pre-release hook is customized, built-in tests/lint are skipped for the entire releasable.

Args:

  • workspace_root: path to the monorepo root.
  • releasable_name: name of the releasable.
  • config: optional releasable-level config dict. When None, uses

script-hash check only (backward compat).

Returns:

  • True if the releasable has a customized pre-release hook, False otherwise.