All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 24s
CD Pipeline / build-and-deploy (push) Successful in 4m52s
CD Pipeline / post-deploy-checks (push) Successful in 58s
108 lines
3.6 KiB
Python
108 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Guard AWOOOI product navigation against orphaned frontend routes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
ROUTE_RE = re.compile(r"href:\s*'([^']+)'")
|
|
ROUTE_LIST_RE = re.compile(r"(?:aliases|relatedPaths):\s*\[([^\]]*)\]")
|
|
LABEL_KEY_RE = re.compile(r"labelKey:\s*'([^']+)'")
|
|
QUOTED_RE = re.compile(r"'([^']+)'")
|
|
|
|
|
|
def locale_routes(root: Path) -> set[str]:
|
|
app_root = root / "apps/web/src/app/[locale]"
|
|
routes: set[str] = set()
|
|
for page in app_root.rglob("page.tsx"):
|
|
rel = page.relative_to(app_root)
|
|
route_parts = rel.parts[:-1]
|
|
if any(part.startswith("[") and part.endswith("]") for part in route_parts):
|
|
continue
|
|
route = "/" + "/".join(route_parts)
|
|
routes.add("/" if route == "/" else route.rstrip("/"))
|
|
return routes
|
|
|
|
|
|
def navigation_routes(root: Path) -> set[str]:
|
|
source = (root / "apps/web/src/lib/navigation/product-ia.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
routes = set(ROUTE_RE.findall(source))
|
|
for match in ROUTE_LIST_RE.findall(source):
|
|
routes.update(QUOTED_RE.findall(match))
|
|
return {route.rstrip("/") or "/" for route in routes}
|
|
|
|
|
|
def navigation_label_keys(root: Path) -> set[str]:
|
|
source = (root / "apps/web/src/lib/navigation/product-ia.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
return set(LABEL_KEY_RE.findall(source))
|
|
|
|
|
|
def missing_nav_labels(root: Path, label_keys: set[str]) -> dict[str, list[str]]:
|
|
missing: dict[str, list[str]] = {}
|
|
for messages_file in sorted((root / "apps/web/messages").glob("*.json")):
|
|
with messages_file.open(encoding="utf-8") as handle:
|
|
payload = json.load(handle)
|
|
nav = payload.get("nav") if isinstance(payload, dict) else None
|
|
nav_keys = set(nav) if isinstance(nav, dict) else set()
|
|
gaps = sorted(label_keys - nav_keys)
|
|
if gaps:
|
|
missing[messages_file.name] = gaps
|
|
return missing
|
|
|
|
|
|
def covered(route: str, nav_routes: set[str]) -> bool:
|
|
if route in nav_routes:
|
|
return True
|
|
return any(parent != "/" and route.startswith(parent + "/") for parent in nav_routes)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", default=".", help="Repository root")
|
|
args = parser.parse_args()
|
|
|
|
root = Path(args.root).resolve()
|
|
routes = locale_routes(root)
|
|
nav_routes = navigation_routes(root)
|
|
label_keys = navigation_label_keys(root)
|
|
missing = sorted(route for route in routes if not covered(route, nav_routes))
|
|
missing_labels = missing_nav_labels(root, label_keys)
|
|
|
|
sidebar = (root / "apps/web/src/components/layout/sidebar.tsx").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
hidden_secondary_filter = "surface !== 'secondary'" in sidebar
|
|
|
|
if missing or missing_labels or hidden_secondary_filter:
|
|
if missing:
|
|
print("NAVIGATION_COVERAGE_GUARD_MISSING_ROUTES")
|
|
for route in missing:
|
|
print(f"- {route}")
|
|
if missing_labels:
|
|
print("NAVIGATION_COVERAGE_GUARD_MISSING_LABEL_KEYS")
|
|
for file_name, gaps in missing_labels.items():
|
|
print(f"- {file_name}: {', '.join(gaps)}")
|
|
if hidden_secondary_filter:
|
|
print("NAVIGATION_COVERAGE_GUARD_HIDDEN_SECONDARY_ITEMS")
|
|
print("- Sidebar must not filter product nav items by surface !== 'secondary'.")
|
|
return 1
|
|
|
|
print(
|
|
"AWOOOI_NAVIGATION_COVERAGE_GUARD_OK "
|
|
f"routes={len(routes)} nav_routes={len(nav_routes)} nav_label_keys={len(label_keys)}"
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|