1414
1515import argparse
1616import re
17+ import shutil
1718import subprocess
1819import sys
1920import tomllib
@@ -156,8 +157,17 @@ def rewrite_version_in_files(paths: list[Path], *, old: str, new: str) -> None:
156157 path .write_text (rewrite_version_strings (original , old = old , new = new ), encoding = "utf-8" )
157158
158159
160+ def require_executable (name : str , * , recovery : str ) -> str :
161+ path = shutil .which (name )
162+ if path is None :
163+ raise ReleaseError (f"required executable `{ name } ` not found on PATH; { recovery } " )
164+ return path
165+
166+
159167def sync_docs_changelog () -> None :
160- subprocess .run (["node" , str (DOCS_CHANGELOG_SCRIPT )], cwd = REPO_ROOT / "docs" , check = True )
168+ """Regenerate the docs changelog; validate() preflights node before mutations."""
169+ node = require_executable ("node" , recovery = "install Node.js or add `node` to PATH" )
170+ subprocess .run ([node , str (DOCS_CHANGELOG_SCRIPT )], cwd = REPO_ROOT / "docs" , check = True )
161171
162172
163173def _run (cmd : list [str ], * , dry_run : bool , check : bool = True ) -> subprocess .CompletedProcess [str ]:
@@ -177,6 +187,7 @@ def _git_capture(cmd: list[str]) -> str:
177187def validate (target : str ) -> None :
178188 """Phase 1 — fail loud, no writes."""
179189 parse_semver (target )
190+ require_executable ("node" , recovery = "install Node.js or add `node` to PATH" )
180191 if _git_capture (["git" , "status" , "--porcelain" ]):
181192 raise ReleaseError ("working tree is not clean; commit or stash first" )
182193 _git_capture (["git" , "fetch" , "origin" ])
0 commit comments