feat(check): postgres preflight in scripts/check.py
新增 check_postgres_preflight() —— make dev 链 (check.py → serve.sh) 的最后一道 preflight: - config.yaml 不存在 → silent skip(让 setup_wizard 引导) - backend != postgres → silent skip - DATABASE_URL 未设 → FAIL with hint - postgres 设了但 host:port 3s socket 不通 → FAIL with 启 docker 提示 - 通则 OK + 显示 host:port 不在 serve.sh 里加:Makefile 已经把 check.py 串在 serve.sh 之前,FAIL 会 自然阻断启动;避免 bash + python 两处实现 PG 探测。 doctor.py 的 check_database 是事后诊断(make doctor);本 check 是事前 preflight(make dev/start)—— 互补。 Stage 0 PR2 T2.6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
def configure_stdio() -> None:
|
def configure_stdio() -> None:
|
||||||
@@ -57,6 +60,80 @@ def parse_node_major(version_text: str) -> int | None:
|
|||||||
return int(major_str)
|
return int(major_str)
|
||||||
|
|
||||||
|
|
||||||
|
def check_postgres_preflight() -> tuple[bool, str | None]:
|
||||||
|
"""If config.yaml selects postgres, verify the URL is set + host:port reachable.
|
||||||
|
|
||||||
|
Returns ``(ok, message)``. Skips silently when:
|
||||||
|
- config.yaml does not exist (user hasn't run 'make setup' yet)
|
||||||
|
- database.backend is not 'postgres' (sqlite/memory don't need preflight)
|
||||||
|
- DATABASE_URL env var is not set (user hasn't filled .env yet)
|
||||||
|
|
||||||
|
Fails when database.backend is 'postgres' AND DATABASE_URL parses but the
|
||||||
|
host:port socket cannot be opened in 3 seconds.
|
||||||
|
"""
|
||||||
|
repo_root = Path(__file__).resolve().parent.parent
|
||||||
|
config_path = repo_root / "config.yaml"
|
||||||
|
if not config_path.exists():
|
||||||
|
return True, None # No config yet — let setup_wizard handle it
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml # type: ignore[import-not-found]
|
||||||
|
except ImportError:
|
||||||
|
return True, "PyYAML not installed; skipping Postgres preflight"
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
||||||
|
except Exception as exc:
|
||||||
|
return False, f"Failed to parse config.yaml: {exc}"
|
||||||
|
|
||||||
|
db = data.get("database") or {}
|
||||||
|
backend = (db.get("backend") or "sqlite").lower()
|
||||||
|
if backend != "postgres":
|
||||||
|
return True, None # not on PG, no preflight needed
|
||||||
|
|
||||||
|
raw_url = db.get("postgres_url") or db.get("url") or ""
|
||||||
|
if isinstance(raw_url, str) and raw_url.startswith("$"):
|
||||||
|
env_name = raw_url[1:]
|
||||||
|
# Load .env if available so this preflight matches what serve.sh sees
|
||||||
|
env_path = repo_root / ".env"
|
||||||
|
if env_path.exists():
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv # type: ignore[import-not-found]
|
||||||
|
|
||||||
|
load_dotenv(env_path, override=False)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
url = os.environ.get(env_name, "")
|
||||||
|
if not url:
|
||||||
|
return False, f"database.backend=postgres but {env_name} is not set in .env"
|
||||||
|
else:
|
||||||
|
url = raw_url
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
return False, "database.backend=postgres but no postgres_url is configured"
|
||||||
|
|
||||||
|
parsed = urlparse(url.replace("postgresql+asyncpg://", "postgresql://"))
|
||||||
|
host = parsed.hostname
|
||||||
|
port = parsed.port or 5432
|
||||||
|
if not host:
|
||||||
|
return False, f"Cannot parse host from DATABASE_URL: {url[:60]}..."
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(3)
|
||||||
|
try:
|
||||||
|
sock.connect((host, port))
|
||||||
|
return True, f"Postgres reachable at {host}:{port}"
|
||||||
|
except OSError as exc:
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
f"Postgres unreachable at {host}:{port} ({exc}). "
|
||||||
|
f"Run 'docker compose -f docker/docker-compose-dev.yaml up -d postgres' "
|
||||||
|
f"or check DATABASE_URL.",
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
configure_stdio()
|
configure_stdio()
|
||||||
print("==========================================")
|
print("==========================================")
|
||||||
@@ -143,6 +220,18 @@ def main() -> int:
|
|||||||
print(" Or visit: https://nginx.org/en/download.html")
|
print(" Or visit: https://nginx.org/en/download.html")
|
||||||
failed = True
|
failed = True
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Checking Postgres preflight...")
|
||||||
|
pg_ok, pg_msg = check_postgres_preflight()
|
||||||
|
if pg_ok:
|
||||||
|
if pg_msg:
|
||||||
|
print(f" OK {pg_msg}")
|
||||||
|
else:
|
||||||
|
print(" -- skipped (config.yaml missing or backend != postgres)")
|
||||||
|
else:
|
||||||
|
print(f" FAIL {pg_msg}")
|
||||||
|
failed = True
|
||||||
|
|
||||||
print()
|
print()
|
||||||
if not failed:
|
if not failed:
|
||||||
print("==========================================")
|
print("==========================================")
|
||||||
|
|||||||
Reference in New Issue
Block a user