#!/usr/bin/env python3 """Lightweight readiness checks for the ci-test TiDB branch. This intentionally checks only the minimum platform baseline required by the E2E CI reset flow. It is not a replacement for full `cmd/setup verify`. """ from __future__ import annotations import json import os import sys import time from dataclasses import dataclass from typing import Callable, List, Optional, Sequence, Tuple import pymysql @dataclass class CheckResult: name: str ok: bool detail: str elapsed_ms: int class BaselineVerifier: def __init__(self) -> None: self.conn = pymysql.connect( host=self._env("TIDB_HOST"), port=int(os.getenv("TIDB_PORT", "4000")), user=self._env("TIDB_USER"), password=self._env("TIDB_PASSWORD"), database=os.getenv("TIDB_DATABASE", "nexuscrm"), ssl={"ca": None}, ssl_verify_cert=False, connect_timeout=10, read_timeout=10, write_timeout=10, cursorclass=pymysql.cursors.Cursor, ) self.results: List[CheckResult] = [] @staticmethod def _env(key: str) -> str: value = os.getenv(key) if not value: raise RuntimeError(f"missing required env: {key}") return value def close(self) -> None: self.conn.close() def query_one(self, sql: str, params: Optional[Sequence[object]] = None) -> Tuple[object, ...]: with self.conn.cursor() as cur: cur.execute(sql, params or ()) row = cur.fetchone() if row is None: raise RuntimeError(f"query returned no rows: {sql}") return row def check(self, name: str, fn: Callable[[], str]) -> None: started = time.time() try: detail = fn() ok = True except Exception as exc: # noqa: BLE001 - we want a terse CI failure detail detail = str(exc) ok = False elapsed_ms = int((time.time() - started) * 1000) self.results.append(CheckResult(name=name, ok=ok, detail=detail, elapsed_ms=elapsed_ms)) def run(self) -> int: self.check("db_connectivity", self._check_connectivity) self.check("bootstrap_state", self._check_bootstrap_state) self.check("system_object_row", self._check_system_object_row) self.check("system_field_row", self._check_system_field_row) self.check("connected_app_table", self._check_connected_app_table) self.check("nexuscrm_ui_connected_app", self._check_nexuscrm_ui) failed = [r for r in self.results if not r.ok] print(self._json_report()) summary = os.getenv("GITHUB_STEP_SUMMARY") if summary: self._append_summary(summary) return 1 if failed else 0 def _check_connectivity(self) -> str: row = self.query_one("SELECT 1") if row[0] != 1: raise RuntimeError(f"unexpected SELECT 1 result: {row[0]}") return "SELECT 1 ok" def _check_bootstrap_state(self) -> str: row = self.query_one( """ SELECT Phase, Status, RunID FROM `_System_BootstrapState` ORDER BY LastModifiedDate DESC LIMIT 1 """ ) phase, status, run_id = row if str(status).lower() != "completed": raise RuntimeError(f"latest bootstrap status={status}, phase={phase}, run_id={run_id}") return f"latest run {run_id} phase={phase} status={status}" def _check_system_object_row(self) -> str: row = self.query_one("SELECT COUNT(*) FROM `_System_Object` WHERE LOWER(`APIName`) = '_system_connectedapp'") count = int(row[0]) if count == 0: raise RuntimeError("_System_Object missing api_name=_System_ConnectedApp") return f"count={count}" def _check_system_field_row(self) -> str: row = self.query_one( """ SELECT COUNT(*) FROM `_System_Field` f JOIN `_System_Object` o ON o.`Id` = f.`ObjectId` WHERE LOWER(o.`APIName`) = '_system_connectedapp' AND LOWER(f.`ApiName`) = 'consumerkey' """ ) count = int(row[0]) if count == 0: raise RuntimeError("_System_Field missing _System_ConnectedApp.ConsumerKey") return f"count={count}" def _check_connected_app_table(self) -> str: row = self.query_one( """ SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '_System_ConnectedApp' """ ) count = int(row[0]) if count == 0: raise RuntimeError("physical table _System_ConnectedApp does not exist") return "table exists" def _check_nexuscrm_ui(self) -> str: row = self.query_one("SELECT COUNT(*) FROM `_System_ConnectedApp` WHERE `ConsumerKey` = 'nexuscrm-ui'") count = int(row[0]) if count == 0: raise RuntimeError("required connected app nexuscrm-ui is missing") return f"count={count}" def _json_report(self) -> str: payload = { "ok": all(r.ok for r in self.results), "checks": [ { "name": r.name, "ok": r.ok, "detail": r.detail, "elapsed_ms": r.elapsed_ms, } for r in self.results ], } return json.dumps(payload, ensure_ascii=False, indent=2) def _append_summary(self, path: str) -> None: with open(path, "a", encoding="utf-8") as fp: fp.write("### CI-Test Baseline Verify\n\n") fp.write("| Check | Status | Detail | Time |\n") fp.write("|---|---|---|---|\n") for r in self.results: status = "PASS" if r.ok else "FAIL" detail = r.detail.replace("\n", " ") fp.write(f"| `{r.name}` | {status} | {detail} | {r.elapsed_ms}ms |\n") fp.write("\n") def main() -> int: verifier: Optional[BaselineVerifier] = None try: verifier = BaselineVerifier() return verifier.run() except Exception as exc: # noqa: BLE001 - top-level CI failure detail print(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False, indent=2)) return 1 finally: if verifier is not None: verifier.close() if __name__ == "__main__": sys.exit(main())