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:
1445043649
2026-05-11 09:17:19 +08:00
parent 3e62a0f6ef
commit 83b680b2ea
+89
View File
@@ -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("==========================================")