gabriel / muse public
hooks.py python
120 lines 3.7 KB
Raw
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