ci: enforce endpoint inventory independently
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user