Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 14s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
146 lines
5.6 KiB
Python
146 lines
5.6 KiB
Python
"""P0-004 CI/CD baseline source readiness.
|
|
|
|
Loads the committed source-readiness snapshot for the dev/prod CI/CD baseline.
|
|
This is read-only: it checks committed source paths and does not modify
|
|
workflows, trigger Gitea, create repos, sync refs, read secrets, or touch hosts.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from src.services.snapshot_paths import default_operations_dir, resolve_repo_root
|
|
|
|
_DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__))
|
|
_SNAPSHOT_FILE = "p0-cicd-baseline-source-readiness.snapshot.json"
|
|
_SCHEMA_VERSION = "p0_cicd_baseline_source_readiness_v1"
|
|
|
|
|
|
def load_latest_p0_cicd_baseline_source_readiness(
|
|
operations_dir: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Load the committed P0-004 CI/CD baseline source readiness snapshot."""
|
|
directory = operations_dir or _DEFAULT_OPERATIONS_DIR
|
|
path = directory / _SNAPSHOT_FILE
|
|
with path.open(encoding="utf-8") as handle:
|
|
payload = json.load(handle)
|
|
|
|
if not isinstance(payload, dict):
|
|
raise ValueError(f"{path}: expected JSON object")
|
|
repo_root = resolve_repo_root(path)
|
|
_enrich_source_presence(payload, repo_root)
|
|
_require_schema(payload, str(path))
|
|
_require_operation_boundaries(payload, str(path))
|
|
_require_rollup_consistency(payload, str(path))
|
|
return payload
|
|
|
|
|
|
def _enrich_source_presence(payload: dict[str, Any], repo_root: Path) -> None:
|
|
sources = _list(payload.get("required_sources"))
|
|
required_sources = [source for source in sources if _dict(source).get("required") is True]
|
|
present_ids: list[str] = []
|
|
missing_ids: list[str] = []
|
|
for source in required_sources:
|
|
item = _dict(source)
|
|
relative_path = str(item.get("path") or "")
|
|
present = bool(relative_path) and _source_present(repo_root, relative_path)
|
|
item["present"] = present
|
|
if present:
|
|
present_ids.append(str(item.get("id") or relative_path))
|
|
else:
|
|
missing_ids.append(str(item.get("id") or relative_path))
|
|
|
|
required_count = len(required_sources)
|
|
present_count = len(present_ids)
|
|
missing_count = len(missing_ids)
|
|
rollups = _dict(payload.get("rollups"))
|
|
rollups["required_source_count"] = required_count
|
|
rollups["present_required_source_count"] = present_count
|
|
rollups["missing_required_source_count"] = missing_count
|
|
rollups["source_readiness_percent"] = round(
|
|
present_count / max(required_count, 1) * 100
|
|
)
|
|
rollups["blocked_source_ids"] = missing_ids
|
|
rollups["hard_blocker_count"] = len(_list(payload.get("blockers")))
|
|
rollups["next_action_count"] = len(_list(payload.get("next_actions")))
|
|
payload["status"] = (
|
|
"ready_for_template_copy_apply_gate"
|
|
if missing_count == 0
|
|
else "blocked_required_sources_missing"
|
|
)
|
|
|
|
|
|
def _source_present(repo_root: Path, relative_path: str) -> bool:
|
|
if (repo_root / relative_path).is_file():
|
|
return True
|
|
|
|
api_prefix = "apps/api/"
|
|
if relative_path.startswith(api_prefix):
|
|
runtime_relative_path = relative_path.removeprefix(api_prefix)
|
|
return (repo_root / runtime_relative_path).is_file()
|
|
|
|
return False
|
|
|
|
|
|
def _require_schema(payload: dict[str, Any], label: str) -> None:
|
|
actual = payload.get("schema_version")
|
|
if actual != _SCHEMA_VERSION:
|
|
raise ValueError(
|
|
f"{label}: expected schema_version={_SCHEMA_VERSION}, got {actual!r}"
|
|
)
|
|
|
|
|
|
def _require_operation_boundaries(payload: dict[str, Any], label: str) -> None:
|
|
boundaries = _dict(payload.get("operation_boundaries"))
|
|
if boundaries.get("read_only_api_allowed") is not True:
|
|
raise ValueError(f"{label}: read_only_api_allowed must be true")
|
|
blocked_flags = {
|
|
"workflow_modification_allowed",
|
|
"workflow_trigger_allowed",
|
|
"repo_creation_allowed",
|
|
"refs_sync_allowed",
|
|
"github_api_allowed",
|
|
"host_or_k8s_write_allowed",
|
|
"secret_read_allowed",
|
|
"raw_session_or_sqlite_read_allowed",
|
|
}
|
|
allowed = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False)
|
|
if allowed:
|
|
raise ValueError(f"{label}: operation boundaries must remain false: {allowed}")
|
|
|
|
|
|
def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None:
|
|
sources = [
|
|
source
|
|
for source in _list(payload.get("required_sources"))
|
|
if _dict(source).get("required") is True
|
|
]
|
|
blockers = _list(payload.get("blockers"))
|
|
next_actions = _list(payload.get("next_actions"))
|
|
rollups = _dict(payload.get("rollups"))
|
|
missing_ids = [
|
|
str(_dict(source).get("id") or _dict(source).get("path") or "")
|
|
for source in sources
|
|
if _dict(source).get("present") is not True
|
|
]
|
|
if rollups.get("required_source_count") != len(sources):
|
|
raise ValueError(f"{label}: required_source_count must match sources")
|
|
if rollups.get("missing_required_source_count") != len(missing_ids):
|
|
raise ValueError(f"{label}: missing_required_source_count mismatch")
|
|
if rollups.get("blocked_source_ids") != missing_ids:
|
|
raise ValueError(f"{label}: blocked_source_ids must match missing sources")
|
|
if rollups.get("hard_blocker_count") != len(blockers):
|
|
raise ValueError(f"{label}: hard_blocker_count must match blockers")
|
|
if rollups.get("next_action_count") != len(next_actions):
|
|
raise ValueError(f"{label}: next_action_count must match next_actions")
|
|
|
|
|
|
def _dict(value: Any) -> dict[str, Any]:
|
|
return value if isinstance(value, dict) else {}
|
|
|
|
|
|
def _list(value: Any) -> list[Any]:
|
|
return value if isinstance(value, list) else []
|