Skip to content
rlsbl.commands.release.hooks
On this page

Hook helpers: content hashing, template hash lookup, hook emptiness check, and shell script runner for pre-checks, pre-release, and post-release hooks.

#rlsbl.commands.release.hooks

#rlsbl.commands.release.hooks

Hook helpers: content hashing, template hash lookup, hook emptiness check, and shell script runner for pre-checks, pre-release, and post-release hooks.

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": <string>}. 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"][<hook_config_key>], 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_package_hook_path

python
def get_package_hook_path(package_dir, hook_name)

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

Path: <package_dir>/.rlsbl/hooks/<hook_name>

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.

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