#!/usr/bin/env python3 from __future__ import annotations import argparse import json import os import sys from pathlib import Path from typing import Any, Dict, List REPO_ROOT = Path(__file__).resolve().parents[2] BACKEND_DIR = REPO_ROOT / "backend" if str(BACKEND_DIR) not in sys.path: sys.path.insert(0, str(BACKEND_DIR)) from app.auth import load_auth_settings # noqa: E402 def _is_placeholder_key(value: str) -> bool: v = str(value or "").strip() if not v: return True vu = v.upper() return vu.startswith("YOUR_KEY") or vu in {"", "YOUR_OPENAI_API_KEY", "REPLACE_ME"} def _resolve_router_rag_paths() -> Dict[str, Path]: data_dir = Path(str(os.getenv("ROUTER_RAG_DATA_DIR", "") or "").strip() or (REPO_ROOT / "_RAG_Ready_KB_Organized")) chunks = Path(str(os.getenv("ROUTER_RAG_CHUNKS_PATH", "") or "").strip() or (data_dir / "04_ingestion" / "rag_ingestion_chunks.jsonl")) manifest = Path(str(os.getenv("ROUTER_RAG_MANIFEST_PATH", "") or "").strip() or (data_dir / "03_manifests" / "rag_manifest_organized.csv")) return {"data_dir": data_dir, "chunks": chunks, "manifest": manifest} def _check_path(path: Path, *, file_required: bool = True) -> Dict[str, Any]: exists = path.exists() ok = exists and (path.is_file() if file_required else path.is_dir()) return {"path": str(path), "exists": exists, "ok": ok} def _build_report(args: argparse.Namespace) -> Dict[str, Any]: checks: Dict[str, Any] = {} failures: List[str] = [] warnings: List[str] = [] openai_key = str(os.getenv("OPENAI_API_KEY", "") or "").strip() openai_ok = bool(openai_key) and not _is_placeholder_key(openai_key) checks["openai"] = { "required": bool(args.require_openai), "present": bool(openai_key), "placeholder": _is_placeholder_key(openai_key) if openai_key else True, "ok": openai_ok or (not args.require_openai), } if args.require_openai and not openai_ok: failures.append("OPENAI_API_KEY is missing or placeholder.") auth = load_auth_settings() checks["auth"] = { "required": bool(auth.required), "enabled": bool(auth.enabled), "config_error": auth.config_error, "config_details": list(auth.config_details), "config_warnings": list(auth.config_warnings), "ok": (not auth.required) or bool(auth.enabled), } if auth.required and not auth.enabled: failures.append(auth.config_error or "Auth is required but Auth0 config is invalid.") if auth.config_warnings: warnings.extend(list(auth.config_warnings)) repo_assets = { "routers_eos_eol_by_sku.csv": REPO_ROOT / "routers_eos_eol_by_sku.csv", "feb2026routers.csv": REPO_ROOT / "feb2026routers.csv", "FAQ_master_updated.csv": REPO_ROOT / "docs" / "faq" / "FAQ_master_updated.csv", } checks["required_assets"] = {name: _check_path(path, file_required=True) for name, path in repo_assets.items()} for name, data in checks["required_assets"].items(): if not bool(data.get("ok")): failures.append(f"Missing required asset: {name}") rag = _resolve_router_rag_paths() checks["router_rag"] = { "data_dir": _check_path(rag["data_dir"], file_required=False), "chunks": _check_path(rag["chunks"], file_required=True), "manifest": _check_path(rag["manifest"], file_required=True), "strict": bool(args.fail_on_missing_router_rag), } if args.fail_on_missing_router_rag: if not checks["router_rag"]["chunks"]["ok"]: failures.append("Router RAG chunks file is missing.") if not checks["router_rag"]["manifest"]["ok"]: failures.append("Router RAG manifest file is missing.") else: if not checks["router_rag"]["chunks"]["ok"] or not checks["router_rag"]["manifest"]["ok"]: warnings.append("Router RAG ingestion artifacts are missing; router-doc retrieval may be degraded.") return { "ok": not failures, "failures": failures, "warnings": warnings, "checks": checks, } def main() -> int: parser = argparse.ArgumentParser(description="Preflight environment/configuration checks for CI and release gates.") parser.add_argument("--require-openai", action="store_true", help="Fail if OPENAI_API_KEY is missing/placeholder.") parser.add_argument( "--fail-on-missing-router-rag", action="store_true", help="Fail if router RAG manifest/chunks files are missing.", ) parser.add_argument("--out", default="", help="Optional output JSON path.") args = parser.parse_args() report = _build_report(args) payload = json.dumps(report, indent=2) if args.out: out_path = Path(args.out) out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(payload + "\n", encoding="utf-8") print(f"Wrote preflight report: {out_path}") print(payload) return 0 if report.get("ok") else 2 if __name__ == "__main__": raise SystemExit(main())