Prepare GovOPlaN self-hosted release workflow
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
@@ -36,6 +38,23 @@ PREFIXES = {
|
||||
}
|
||||
FEATURE_OWNERS = ("files", "mail", "campaign")
|
||||
PLATFORM_OWNERS = ("admin", "tenancy", "organizations", "identity", "policy", "audit", "dashboard")
|
||||
WEBUI_REPOS = {
|
||||
"core": ROOT / "webui",
|
||||
"files": ROOT.parent / "govoplan-files" / "webui",
|
||||
"mail": ROOT.parent / "govoplan-mail" / "webui",
|
||||
"campaign": ROOT.parent / "govoplan-campaign" / "webui",
|
||||
}
|
||||
WEBUI_PACKAGES = {
|
||||
"core": "@govoplan/core-webui",
|
||||
"files": "@govoplan/files-webui",
|
||||
"mail": "@govoplan/mail-webui",
|
||||
"campaign": "@govoplan/campaign-webui",
|
||||
}
|
||||
WEBUI_IMPORT_RE = re.compile(
|
||||
r"(?:from\s+|import\s*\(\s*|require\s*\(\s*|export\s+[^;]*?\s+from\s+)"
|
||||
r"[\"'](?P<package>@govoplan/[a-z0-9_-]+-webui)(?:/[A-Za-z0-9_./-]+)?[\"']"
|
||||
r"|import\s+[\"'](?P<side_effect_package>@govoplan/[a-z0-9_-]+-webui)(?:/[A-Za-z0-9_./-]+)?[\"']"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -54,6 +73,7 @@ ALLOWLIST_KEYS = {(item.owner, item.relative_path, item.imported_owner) for item
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Violation:
|
||||
kind: str
|
||||
owner: str
|
||||
path: Path
|
||||
lineno: int
|
||||
@@ -102,17 +122,97 @@ def _violations() -> list[Violation]:
|
||||
continue
|
||||
if owner == "core" and imported_owner in FEATURE_OWNERS:
|
||||
if not _allowed(owner, path, imported_owner):
|
||||
violations.append(Violation(owner, path, lineno, imported, imported_owner))
|
||||
violations.append(Violation("python", owner, path, lineno, imported, imported_owner))
|
||||
elif owner == "access" and imported_owner in FEATURE_OWNERS:
|
||||
violations.append(Violation(owner, path, lineno, imported, imported_owner))
|
||||
violations.append(Violation("python", owner, path, lineno, imported, imported_owner))
|
||||
elif owner in PLATFORM_OWNERS and imported_owner == "access":
|
||||
if not _allowed(owner, path, imported_owner):
|
||||
violations.append(Violation(owner, path, lineno, imported, imported_owner))
|
||||
violations.append(Violation("python", owner, path, lineno, imported, imported_owner))
|
||||
elif owner in FEATURE_OWNERS and imported_owner in FEATURE_OWNERS:
|
||||
if not _allowed(owner, path, imported_owner):
|
||||
violations.append(Violation(owner, path, lineno, imported, imported_owner))
|
||||
violations.append(Violation("python", owner, path, lineno, imported, imported_owner))
|
||||
elif owner in FEATURE_OWNERS and imported_owner == "access":
|
||||
violations.append(Violation(owner, path, lineno, imported, imported_owner))
|
||||
violations.append(Violation("python", owner, path, lineno, imported, imported_owner))
|
||||
violations.extend(_webui_violations())
|
||||
return violations
|
||||
|
||||
|
||||
def _webui_package_owner(package_name: str) -> str | None:
|
||||
for owner, package in WEBUI_PACKAGES.items():
|
||||
if package_name == package:
|
||||
return owner
|
||||
return None
|
||||
|
||||
|
||||
def _webui_source_files(root: Path) -> list[Path]:
|
||||
source_root = root / "src"
|
||||
if not source_root.exists():
|
||||
return []
|
||||
return [
|
||||
path
|
||||
for path in source_root.rglob("*")
|
||||
if path.suffix in {".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}
|
||||
and "node_modules" not in path.parts
|
||||
and "dist" not in path.parts
|
||||
]
|
||||
|
||||
|
||||
def _webui_dependency_fields(payload: object) -> dict[str, object]:
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
result: dict[str, object] = {}
|
||||
for key in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"):
|
||||
value = payload.get(key)
|
||||
if isinstance(value, dict):
|
||||
result.update(value)
|
||||
return result
|
||||
|
||||
|
||||
def _webui_package_json_violations(owner: str, root: Path) -> list[Violation]:
|
||||
violations: list[Violation] = []
|
||||
package_paths = [root / "package.json"]
|
||||
if root.name == "webui":
|
||||
package_paths.append(root.parent / "package.json")
|
||||
for package_path in tuple(dict.fromkeys(package_paths)):
|
||||
if not package_path.exists():
|
||||
continue
|
||||
try:
|
||||
payload = json.loads(package_path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
return [Violation("webui-package", owner, package_path, exc.lineno, "invalid package.json", owner)]
|
||||
for package_name in _webui_dependency_fields(payload):
|
||||
imported_owner = _webui_package_owner(str(package_name))
|
||||
if imported_owner is None or imported_owner == owner:
|
||||
continue
|
||||
if owner in FEATURE_OWNERS and imported_owner in FEATURE_OWNERS:
|
||||
violations.append(Violation("webui-package", owner, package_path, 1, str(package_name), imported_owner))
|
||||
return violations
|
||||
|
||||
|
||||
def _webui_source_violations(owner: str, root: Path) -> list[Violation]:
|
||||
violations: list[Violation] = []
|
||||
for path in _webui_source_files(root):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for match in WEBUI_IMPORT_RE.finditer(text):
|
||||
package_name = match.group("package") or match.group("side_effect_package")
|
||||
imported_owner = _webui_package_owner(package_name)
|
||||
if imported_owner is None or imported_owner == owner:
|
||||
continue
|
||||
lineno = text.count("\n", 0, match.start()) + 1
|
||||
if owner == "core" and imported_owner in FEATURE_OWNERS:
|
||||
violations.append(Violation("webui-source", owner, path, lineno, package_name, imported_owner))
|
||||
elif owner in FEATURE_OWNERS and imported_owner in FEATURE_OWNERS:
|
||||
violations.append(Violation("webui-source", owner, path, lineno, package_name, imported_owner))
|
||||
return violations
|
||||
|
||||
|
||||
def _webui_violations() -> list[Violation]:
|
||||
violations: list[Violation] = []
|
||||
for owner, root in WEBUI_REPOS.items():
|
||||
if not root.exists():
|
||||
continue
|
||||
violations.extend(_webui_package_json_violations(owner, root))
|
||||
violations.extend(_webui_source_violations(owner, root))
|
||||
return violations
|
||||
|
||||
|
||||
@@ -125,7 +225,7 @@ def main() -> int:
|
||||
print("Dependency boundary violations:")
|
||||
for item in violations:
|
||||
print(
|
||||
f"- {item.path}:{item.lineno}: {item.owner} imports {item.imported} "
|
||||
f"- {item.path}:{item.lineno}: {item.owner} {item.kind} imports {item.imported} "
|
||||
f"({item.imported_owner}); use kernel capability/API/event contracts instead"
|
||||
)
|
||||
print("\nIf this is unavoidable transitional debt, add a reasoned ALLOWLIST entry.")
|
||||
|
||||
Reference in New Issue
Block a user