102 lines
4.1 KiB
Python
102 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Check that release Git dependencies expose their owning WebUI package."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
|
|
|
|
META_ROOT = Path(__file__).resolve().parents[2]
|
|
GIT_REPOSITORY = re.compile(r"/(govoplan-[a-z0-9-]+)\.git#v[^/]+$")
|
|
PARITY_FIELDS = (
|
|
"name", "version", "type", "dependencies", "optionalDependencies",
|
|
"peerDependencies", "peerDependenciesMeta",
|
|
)
|
|
ENTRY_FIELDS = ("main", "module", "types", "exports")
|
|
|
|
|
|
def prefixed_entries(value: object) -> object:
|
|
if isinstance(value, str):
|
|
return "./webui/" + value[2:] if value.startswith("./") else "webui/" + value
|
|
if isinstance(value, dict):
|
|
return {key: prefixed_entries(item) for key, item in value.items()}
|
|
if isinstance(value, list):
|
|
return [prefixed_entries(item) for item in value]
|
|
return value
|
|
|
|
|
|
def entry_paths(value: object) -> list[str]:
|
|
if isinstance(value, str):
|
|
return [value]
|
|
if isinstance(value, dict):
|
|
return [path for item in value.values() for path in entry_paths(item)]
|
|
if isinstance(value, list):
|
|
return [path for item in value for path in entry_paths(item)]
|
|
return []
|
|
|
|
|
|
def facade_issues(repository: Path, *, package_name: str) -> list[str]:
|
|
issues: list[str] = []
|
|
try:
|
|
root = json.loads((repository / "package.json").read_text(encoding="utf-8"))
|
|
webui = json.loads((repository / "webui/package.json").read_text(encoding="utf-8"))
|
|
except (OSError, ValueError) as exc:
|
|
return [f"{repository.name}: cannot read package facades: {exc}"]
|
|
if not isinstance(root, dict) or not isinstance(webui, dict):
|
|
return [f"{repository.name}: package manifests must be JSON objects"]
|
|
if webui.get("name") != package_name:
|
|
issues.append(f"{repository.name}: WebUI name does not match {package_name}")
|
|
for field in PARITY_FIELDS:
|
|
if root.get(field) != webui.get(field):
|
|
issues.append(f"{repository.name}: root {field} differs from owning WebUI package")
|
|
for field in ENTRY_FIELDS:
|
|
expected = prefixed_entries(webui.get(field))
|
|
if root.get(field) != expected:
|
|
issues.append(f"{repository.name}: root {field} must target the corresponding webui/ entry")
|
|
for entry in entry_paths(root.get(field)):
|
|
path = repository / entry
|
|
if not path.resolve().is_relative_to((repository / "webui").resolve()):
|
|
issues.append(f"{repository.name}: {field} entry escapes webui/: {entry}")
|
|
elif "*" not in entry and not path.is_file():
|
|
issues.append(f"{repository.name}: missing {field} entry: {entry}")
|
|
if not root.get("exports") and not root.get("main"):
|
|
issues.append(f"{repository.name}: root package has no WebUI entry point")
|
|
return issues
|
|
|
|
|
|
def check_composition(workspace: Path, *, core_root: Path | None = None) -> tuple[int, list[str]]:
|
|
core = core_root or workspace / "govoplan-core"
|
|
release = json.loads((core / "webui/package.release.json").read_text(encoding="utf-8"))
|
|
checked = 0
|
|
issues: list[str] = []
|
|
for name, reference in release.get("dependencies", {}).items():
|
|
if not name.startswith("@govoplan/"):
|
|
continue
|
|
match = GIT_REPOSITORY.search(reference) if isinstance(reference, str) else None
|
|
if match is None:
|
|
issues.append(f"{name}: release dependency is not a versioned GovOPlaN Git source")
|
|
continue
|
|
checked += 1
|
|
issues.extend(facade_issues(workspace / match.group(1), package_name=name))
|
|
return checked, issues
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--workspace-root", type=Path, default=META_ROOT.parent)
|
|
parser.add_argument("--core-root", type=Path)
|
|
args = parser.parse_args()
|
|
checked, issues = check_composition(args.workspace_root, core_root=args.core_root)
|
|
if issues:
|
|
print("\n".join(issues))
|
|
return 1
|
|
print(f"Release WebUI package facade checks passed for {checked} Git dependencies")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|