hooks.py
python
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2
fix: remove commit_exists filter from have anchors — server…
Sonnet 4.6
patch
21 days ago
| 1 | """Bridge hooks — ``.muse/bridge-hooks.toml``. |
| 2 | |
| 3 | Owns the hook dataclasses and the load/run helpers that execute |
| 4 | pre- and post-bridge shell commands. |
| 5 | """ |
| 6 | |
| 7 | from __future__ import annotations |
| 8 | |
| 9 | import pathlib |
| 10 | import subprocess |
| 11 | import sys |
| 12 | from dataclasses import dataclass, field |
| 13 | |
| 14 | from muse.core.errors import ExitCode |
| 15 | |
| 16 | _Env = dict[str, str] |
| 17 | |
| 18 | |
| 19 | @dataclass(frozen=True, eq=True) |
| 20 | class BridgeHook: |
| 21 | """A single pre or post bridge hook entry.""" |
| 22 | |
| 23 | run: str |
| 24 | on_fail: str # "block" or "warn" |
| 25 | |
| 26 | |
| 27 | @dataclass |
| 28 | class BridgeHooks: |
| 29 | """Parsed contents of .muse/bridge-hooks.toml.""" |
| 30 | |
| 31 | pre_bridge: list[BridgeHook] = field(default_factory=list) |
| 32 | post_bridge: list[BridgeHook] = field(default_factory=list) |
| 33 | |
| 34 | |
| 35 | def load_bridge_hooks(muse_root: pathlib.Path) -> BridgeHooks: |
| 36 | """Read and validate ``.muse/bridge-hooks.toml``. |
| 37 | |
| 38 | Returns an empty :class:`BridgeHooks` when the file is absent. |
| 39 | Raises ``SystemExit`` on malformed TOML or invalid ``on_fail`` values. |
| 40 | """ |
| 41 | import tomllib |
| 42 | |
| 43 | hooks_path = muse_root / ".muse" / "bridge-hooks.toml" |
| 44 | if not hooks_path.exists(): |
| 45 | return BridgeHooks() |
| 46 | |
| 47 | try: |
| 48 | data = tomllib.loads(hooks_path.read_text()) |
| 49 | except Exception as exc: |
| 50 | print(f"Error: bridge-hooks.toml is invalid TOML: {exc}", file=sys.stderr) |
| 51 | raise SystemExit(ExitCode.USER_ERROR) from exc |
| 52 | |
| 53 | def _parse_section(section_name: str) -> list[BridgeHook]: |
| 54 | section = data.get(section_name, {}) |
| 55 | raw_hooks = section.get("hooks", []) |
| 56 | result: list[BridgeHook] = [] |
| 57 | for i, entry in enumerate(raw_hooks): |
| 58 | if "run" not in entry: |
| 59 | print( |
| 60 | f"Error: bridge-hooks.toml [{section_name}] hooks[{i}] is missing 'run'", |
| 61 | file=sys.stderr, |
| 62 | ) |
| 63 | raise SystemExit(ExitCode.USER_ERROR) |
| 64 | on_fail = entry.get("on_fail", "block") |
| 65 | if on_fail not in ("block", "warn"): |
| 66 | print( |
| 67 | f"Error: bridge-hooks.toml [{section_name}] hooks[{i}] " |
| 68 | f"has invalid on_fail={on_fail!r}; must be 'block' or 'warn'", |
| 69 | file=sys.stderr, |
| 70 | ) |
| 71 | raise SystemExit(ExitCode.USER_ERROR) |
| 72 | result.append(BridgeHook(run=entry["run"], on_fail=on_fail)) |
| 73 | return result |
| 74 | |
| 75 | return BridgeHooks( |
| 76 | pre_bridge=_parse_section("pre_bridge"), |
| 77 | post_bridge=_parse_section("post_bridge"), |
| 78 | ) |
| 79 | |
| 80 | |
| 81 | def run_hook(hook: BridgeHook, *, cwd: pathlib.Path, env: _Env) -> None: |
| 82 | """Execute a single bridge hook in ``cwd`` with the given environment. |
| 83 | |
| 84 | Raises ``SystemExit`` when ``on_fail='block'`` and the process exits non-zero. |
| 85 | Prints a warning and continues when ``on_fail='warn'`` and exits non-zero. |
| 86 | """ |
| 87 | import os |
| 88 | import shlex |
| 89 | |
| 90 | merged_env = {**os.environ, **env} |
| 91 | result = subprocess.run( |
| 92 | shlex.split(hook.run), |
| 93 | shell=False, |
| 94 | cwd=cwd, |
| 95 | env=merged_env, |
| 96 | ) |
| 97 | if result.returncode != 0: |
| 98 | if hook.on_fail == "block": |
| 99 | print( |
| 100 | f"Error: bridge hook failed (exit {result.returncode}): {hook.run}", |
| 101 | file=sys.stderr, |
| 102 | ) |
| 103 | raise SystemExit(result.returncode) |
| 104 | else: |
| 105 | print( |
| 106 | f"Warning: bridge hook exited {result.returncode} (continuing): {hook.run}", |
| 107 | file=sys.stderr, |
| 108 | ) |
| 109 | |
| 110 | |
| 111 | def run_hooks( |
| 112 | hooks: list[BridgeHook], *, cwd: pathlib.Path, env: dict[str, str] |
| 113 | ) -> None: |
| 114 | """Run a list of bridge hooks in order. |
| 115 | |
| 116 | Stops immediately when a ``block`` hook fails (re-raises ``SystemExit``). |
| 117 | Warn hooks that fail print a message but do not stop the chain. |
| 118 | """ |
| 119 | for hook in hooks: |
| 120 | run_hook(hook, cwd=cwd, env=env) |
File History
2 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2
fix: remove commit_exists filter from have anchors — server…
Sonnet 4.6
patch
21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e
fix: rename objects→blobs in push client and all stale test…
Sonnet 4.6
patch
22 days ago