mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
d28666c396
Docs under frontend-modern/public/docs are byte-for-byte copies of repo docs, but the only guard was a CI vitest the git hooks never run. On 2026-09-01 two commits (f4886c2dfb,f313882a7b) each edited a mirrored doc without its copy, passed the hooks, and broke main's Frontend job. Add scripts/check_docs_mirror.py with an index-blob --staged mode wired into the pre-commit hook: a commit that stages either side of an out-of-sync pair (or an orphan shipped copy) fails with the exact sync command, while pre-existing drift from other commits only warns. The worktree mode runs as a named step in the public-docs workflow, with unit tests in scripts/tests picked up by the existing runner. The docsLinks vitest stays as the CI backstop. Build-time generation of public/docs was considered and rejected: the shipped set is a curated subset (61 of 421 docs), so generation still needs a hand-maintained manifest while adding build, dev-server, and test-order coupling.
164 lines
5.9 KiB
Python
164 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Guard the shipped-docs mirror check that backs the pre-commit hook."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SCRIPT = ROOT / "scripts" / "check_docs_mirror.py"
|
|
SPEC = importlib.util.spec_from_file_location("check_docs_mirror", SCRIPT)
|
|
assert SPEC is not None and SPEC.loader is not None
|
|
docs_mirror = importlib.util.module_from_spec(SPEC)
|
|
sys.modules[SPEC.name] = docs_mirror
|
|
SPEC.loader.exec_module(docs_mirror)
|
|
|
|
|
|
def run_git(root: Path, *args: str) -> None:
|
|
subprocess.run(
|
|
[
|
|
"git",
|
|
"-C",
|
|
str(root),
|
|
"-c",
|
|
"user.email=test@example.invalid",
|
|
"-c",
|
|
"user.name=test",
|
|
*args,
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
|
|
def write(root: Path, relative: str, content: str) -> None:
|
|
path = root / relative
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content, encoding="utf-8")
|
|
|
|
|
|
class DocsMirrorMappingTest(unittest.TestCase):
|
|
def test_docs_sourced_mapping(self) -> None:
|
|
self.assertEqual(
|
|
docs_mirror.source_for("frontend-modern/public/docs/i18n/de/README.md"),
|
|
"docs/i18n/de/README.md",
|
|
)
|
|
|
|
def test_root_sourced_mapping(self) -> None:
|
|
self.assertEqual(
|
|
docs_mirror.source_for("frontend-modern/public/docs/SECURITY.md"),
|
|
"SECURITY.md",
|
|
)
|
|
|
|
|
|
class DocsMirrorStagedTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self._temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self._temporary.cleanup)
|
|
self.root = Path(self._temporary.name)
|
|
run_git(self.root, "init", "-q")
|
|
|
|
def test_synced_staged_pair_passes(self) -> None:
|
|
write(self.root, "docs/GUIDE.md", "# Guide\n")
|
|
write(self.root, "frontend-modern/public/docs/GUIDE.md", "# Guide\n")
|
|
run_git(self.root, "add", "docs/GUIDE.md", "frontend-modern/public/docs/GUIDE.md")
|
|
|
|
errors, warnings = docs_mirror.check_staged(self.root)
|
|
|
|
self.assertEqual(errors, [])
|
|
self.assertEqual(warnings, [])
|
|
|
|
def test_staged_source_with_stale_mirror_fails(self) -> None:
|
|
write(self.root, "docs/GUIDE.md", "# Guide v1\n")
|
|
write(self.root, "frontend-modern/public/docs/GUIDE.md", "# Guide v1\n")
|
|
run_git(self.root, "add", "-A")
|
|
run_git(self.root, "commit", "-q", "-m", "seed")
|
|
write(self.root, "docs/GUIDE.md", "# Guide v2\n")
|
|
run_git(self.root, "add", "docs/GUIDE.md")
|
|
|
|
errors, warnings = docs_mirror.check_staged(self.root)
|
|
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn("docs/GUIDE.md and frontend-modern/public/docs/GUIDE.md", errors[0])
|
|
self.assertIn("git show :docs/GUIDE.md", errors[0])
|
|
self.assertEqual(warnings, [])
|
|
|
|
def test_staged_root_sourced_doc_with_stale_mirror_fails(self) -> None:
|
|
write(self.root, "SECURITY.md", "# Security v1\n")
|
|
write(self.root, "frontend-modern/public/docs/SECURITY.md", "# Security v1\n")
|
|
run_git(self.root, "add", "-A")
|
|
run_git(self.root, "commit", "-q", "-m", "seed")
|
|
write(self.root, "SECURITY.md", "# Security v2\n")
|
|
run_git(self.root, "add", "SECURITY.md")
|
|
|
|
errors, _warnings = docs_mirror.check_staged(self.root)
|
|
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn("SECURITY.md and frontend-modern/public/docs/SECURITY.md", errors[0])
|
|
|
|
def test_staged_orphan_mirror_fails(self) -> None:
|
|
write(self.root, "frontend-modern/public/docs/NEW.md", "# New\n")
|
|
run_git(self.root, "add", "frontend-modern/public/docs/NEW.md")
|
|
|
|
errors, _warnings = docs_mirror.check_staged(self.root)
|
|
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn("no repo source docs/NEW.md", errors[0])
|
|
|
|
def test_preexisting_drift_only_warns_on_unrelated_commit(self) -> None:
|
|
write(self.root, "docs/GUIDE.md", "# Guide v2\n")
|
|
write(self.root, "frontend-modern/public/docs/GUIDE.md", "# Guide v1\n")
|
|
run_git(self.root, "add", "-A")
|
|
run_git(self.root, "commit", "-q", "-m", "seed drift")
|
|
write(self.root, "unrelated.txt", "x\n")
|
|
run_git(self.root, "add", "unrelated.txt")
|
|
|
|
errors, warnings = docs_mirror.check_staged(self.root)
|
|
|
|
self.assertEqual(errors, [])
|
|
self.assertEqual(len(warnings), 1)
|
|
self.assertIn("docs/GUIDE.md and frontend-modern/public/docs/GUIDE.md", warnings[0])
|
|
|
|
def test_staged_pair_with_github_tree_link_fails(self) -> None:
|
|
content = "See https://github.com/rcourtman/Pulse/blob/main/docs/OTHER.md\n"
|
|
write(self.root, "docs/GUIDE.md", content)
|
|
write(self.root, "frontend-modern/public/docs/GUIDE.md", content)
|
|
run_git(self.root, "add", "-A")
|
|
|
|
errors, _warnings = docs_mirror.check_staged(self.root)
|
|
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn("must not link to", errors[0])
|
|
|
|
|
|
class DocsMirrorWorktreeTest(unittest.TestCase):
|
|
def test_worktree_drift_and_sync(self) -> None:
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary)
|
|
write(root, "docs/GUIDE.md", "# Guide v2\n")
|
|
write(root, "frontend-modern/public/docs/GUIDE.md", "# Guide v1\n")
|
|
write(root, "SECURITY.md", "# Security\n")
|
|
write(root, "frontend-modern/public/docs/SECURITY.md", "# Security\n")
|
|
|
|
errors, checked = docs_mirror.check_worktree(root)
|
|
|
|
self.assertEqual(checked, 2)
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn("cp docs/GUIDE.md frontend-modern/public/docs/GUIDE.md", errors[0])
|
|
|
|
def test_repo_shipped_docs_are_synced(self) -> None:
|
|
errors, checked = docs_mirror.check_worktree(ROOT)
|
|
|
|
self.assertEqual(errors, [])
|
|
self.assertGreater(checked, 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|