ci: enforce endpoint inventory independently
Dependency Audit / dependency-audit (push) Successful in 1m46s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m43s

This commit is contained in:
2026-08-03 20:29:14 +02:00
parent ce5528e3b8
commit 62501d399a
5 changed files with 81 additions and 21 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ jobs:
run: bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui run: bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
- name: Validate platform endpoint surface declarations - name: Validate platform endpoint surface declarations
working-directory: govoplan working-directory: govoplan
run: .venv/bin/python tools/inventory/platform-interface-inventory.py --strict run: .venv/bin/python tools/inventory/platform-interface-inventory.py --strict-endpoints
- name: Validate Search against PostgreSQL - name: Validate Search against PostgreSQL
working-directory: govoplan working-directory: govoplan
env: env:
+9 -6
View File
@@ -45,9 +45,11 @@ The command writes:
- `audit-reports/platform-inventory/platform-interface-inventory.json` - `audit-reports/platform-inventory/platform-interface-inventory.json`
- `audit-reports/platform-inventory/platform-interface-inventory.md` - `audit-reports/platform-inventory/platform-interface-inventory.md`
Use `--strict` in CI. In addition to translation coverage, strict mode requires Use `--strict` for the combined translation and endpoint audit. Use
every backend endpoint without a statically visible WebUI path to have an exact `--strict-endpoints` in the endpoint-surface CI gate so unrelated translation
entry in catalog work cannot disable route classification enforcement. Both strict modes
require every backend endpoint without a statically visible WebUI path to have
an exact entry in
`tools/inventory/endpoint-surface-declarations.json`. The registry is keyed by `tools/inventory/endpoint-surface-declarations.json`. The registry is keyed by
repository, HTTP method, and canonical version-independent path. It accepts: repository, HTTP method, and canonical version-independent path. It accepts:
@@ -80,9 +82,10 @@ A backend route without a static frontend reference is also a review candidate:
public APIs, workers, callbacks, health checks, connectors, and dynamic URL public APIs, workers, callbacks, health checks, connectors, and dynamic URL
assembly are valid explanations. assembly are valid explanations.
`--strict` currently enforces only translation-catalog completeness. Endpoint The module matrix enforces endpoint declarations with `--strict-endpoints`.
and help classifications need narrow reviewed baselines before they can become Combined `--strict` additionally fails when used translation keys are absent
release gates. from generated locale catalogs. Help-text findings remain review candidates
rather than a release gate because dynamic parent components can supply help.
## Admin Information Architecture ## Admin Information Architecture
@@ -141,6 +141,34 @@ class PlatformInterfaceInventoryTests(unittest.TestCase):
self.assertEqual(1, result["summary"]["stale_endpoint_declarations"]) self.assertEqual(1, result["summary"]["stale_endpoint_declarations"])
self.assertIsNone(result["api"]["backend_endpoints"][0]["surface"]) self.assertIsNone(result["api"]["backend_endpoints"][0]["surface"])
def test_endpoint_only_strict_mode_does_not_fail_on_translation_debt(
self,
) -> None:
result = {
"translation_health": {"missing_catalog_entries": ["missing.key"]},
"api": {
"unclassified_endpoints": [],
"stale_endpoint_declarations": [],
},
}
self.assertEqual(
[],
inventory._strict_failures(
result,
check_translations=False,
check_endpoints=True,
),
)
self.assertEqual(
["used translation keys are missing from generated catalogs"],
inventory._strict_failures(
result,
check_translations=True,
check_endpoints=True,
),
)
def test_fastapi_route_scanner_includes_router_prefix(self) -> None: def test_fastapi_route_scanner_includes_router_prefix(self) -> None:
tree = ast.parse( tree = ast.parse(
""" """
@@ -389,6 +389,13 @@
"rationale": "Campaign delivery diagnostic/status API is retained for bounded support and automation consumers.", "rationale": "Campaign delivery diagnostic/status API is retained for bounded support and automation consumers.",
"repository": "govoplan-campaign" "repository": "govoplan-campaign"
}, },
{
"category": "worker_internal",
"method": "POST",
"path": "/campaigns/operations/artifacts/reconcile",
"rationale": "Privileged, bounded artifact recovery operation used by operators and recovery automation rather than an end-user surface.",
"repository": "govoplan-campaign"
},
{ {
"category": "ui_reachable", "category": "ui_reachable",
"method": "PUT", "method": "PUT",
+36 -14
View File
@@ -45,6 +45,11 @@ def main() -> int:
action="store_true", action="store_true",
help="Fail on missing translations or incomplete endpoint-surface declarations.", help="Fail on missing translations or incomplete endpoint-surface declarations.",
) )
parser.add_argument(
"--strict-endpoints",
action="store_true",
help="Fail only on incomplete or stale endpoint-surface declarations.",
)
parser.add_argument( parser.add_argument(
"--endpoint-declarations", "--endpoint-declarations",
type=Path, type=Path,
@@ -80,20 +85,12 @@ def main() -> int:
print(f"Platform inventory JSON: {json_path}") print(f"Platform inventory JSON: {json_path}")
print(f"Platform inventory summary: {markdown_path}") print(f"Platform inventory summary: {markdown_path}")
if args.strict: if args.strict or args.strict_endpoints:
failures: list[str] = [] failures = _strict_failures(
if inventory["translation_health"]["missing_catalog_entries"]: inventory,
failures.append("used translation keys are missing from generated catalogs") check_translations=args.strict,
if inventory["api"]["unclassified_endpoints"]: check_endpoints=True,
failures.append( )
f"{len(inventory['api']['unclassified_endpoints'])} backend "
"endpoints have no WebUI evidence or surface declaration"
)
if inventory["api"]["stale_endpoint_declarations"]:
failures.append(
f"{len(inventory['api']['stale_endpoint_declarations'])} "
"endpoint declarations do not match a backend endpoint"
)
if failures: if failures:
print( print(
"Strict platform inventory failed: " + "; ".join(failures) + ".", "Strict platform inventory failed: " + "; ".join(failures) + ".",
@@ -103,6 +100,31 @@ def main() -> int:
return 0 return 0
def _strict_failures(
inventory: dict[str, Any],
*,
check_translations: bool,
check_endpoints: bool,
) -> list[str]:
failures: list[str] = []
if (
check_translations
and inventory["translation_health"]["missing_catalog_entries"]
):
failures.append("used translation keys are missing from generated catalogs")
if check_endpoints and inventory["api"]["unclassified_endpoints"]:
failures.append(
f"{len(inventory['api']['unclassified_endpoints'])} backend "
"endpoints have no WebUI evidence or surface declaration"
)
if check_endpoints and inventory["api"]["stale_endpoint_declarations"]:
failures.append(
f"{len(inventory['api']['stale_endpoint_declarations'])} "
"endpoint declarations do not match a backend endpoint"
)
return failures
def _resolve_workspace_root(catalog: dict[str, Any]) -> Path: def _resolve_workspace_root(catalog: dict[str, Any]) -> Path:
sibling_root = META_ROOT.parent.resolve() sibling_root = META_ROOT.parent.resolve()
configured_root = Path(str(catalog["default_parent"])).expanduser().resolve() configured_root = Path(str(catalog["default_parent"])).expanduser().resolve()