migrate_all.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. from __future__ import annotations
  2. import argparse
  3. import os
  4. import subprocess
  5. import sys
  6. from dataclasses import dataclass
  7. from pathlib import Path
  8. DEFAULT_SERVICE_ORDER = [
  9. "workflow-service",
  10. "session-service",
  11. "tool-service",
  12. "runtime-service",
  13. "model-gateway-service",
  14. "memory-service",
  15. "skill-service",
  16. "agent-service",
  17. "team-service",
  18. "human-service",
  19. "knowledge-service",
  20. "event-service",
  21. "auth-service",
  22. "scheduler-service",
  23. "api-gateway",
  24. ]
  25. @dataclass(frozen=True)
  26. class MigrationTarget:
  27. service_name: str
  28. service_path: Path
  29. alembic_ini_path: Path
  30. def main() -> int:
  31. args = parse_args()
  32. repo_root = Path(__file__).resolve().parents[1]
  33. targets = discover_targets(
  34. repo_root=repo_root,
  35. only_services=args.only,
  36. skip_missing=args.skip_missing)
  37. if args.dry_run:
  38. for target in targets:
  39. print(f"{target.service_name}: {target.alembic_ini_path}")
  40. return 0
  41. failed_services: list[str] = []
  42. for target in targets:
  43. print(f"==> migrating {target.service_name}", flush=True)
  44. result = run_alembic_upgrade(target=target, python_executable=args.python)
  45. if result.returncode != 0:
  46. failed_services.append(target.service_name)
  47. if not args.continue_on_error:
  48. break
  49. if failed_services:
  50. print("migration failed for: " + ", ".join(failed_services), file=sys.stderr)
  51. return 1
  52. print(f"migrated {len(targets)} service(s)")
  53. return 0
  54. def parse_args() -> argparse.Namespace:
  55. parser = argparse.ArgumentParser(description="Run Alembic migrations for all services.")
  56. parser.add_argument(
  57. "--only",
  58. action="append",
  59. default=[],
  60. help="Only migrate a service. Can be passed multiple times.")
  61. parser.add_argument(
  62. "--python",
  63. default=sys.executable,
  64. help="Python executable to use for `python -m alembic`.")
  65. parser.add_argument(
  66. "--continue-on-error",
  67. action="store_true",
  68. help="Continue migrating remaining services if one migration fails.")
  69. parser.add_argument(
  70. "--skip-missing",
  71. action="store_true",
  72. help="Skip services without alembic.ini instead of failing.")
  73. parser.add_argument(
  74. "--dry-run",
  75. action="store_true",
  76. help="Print migration targets without executing migrations.")
  77. return parser.parse_args()
  78. def discover_targets(
  79. *,
  80. repo_root: Path,
  81. only_services: list[str],
  82. skip_missing: bool) -> list[MigrationTarget]:
  83. requested_services = only_services or DEFAULT_SERVICE_ORDER
  84. targets: list[MigrationTarget] = []
  85. for service_name in requested_services:
  86. service_path = repo_root / "services" / service_name
  87. alembic_ini_path = service_path / "alembic.ini"
  88. if not alembic_ini_path.exists():
  89. if skip_missing:
  90. continue
  91. raise FileNotFoundError(f"alembic.ini not found for service: {service_name}")
  92. targets.append(
  93. MigrationTarget(
  94. service_name=service_name,
  95. service_path=service_path,
  96. alembic_ini_path=alembic_ini_path)
  97. )
  98. return targets
  99. def run_alembic_upgrade(
  100. *,
  101. target: MigrationTarget,
  102. python_executable: str) -> subprocess.CompletedProcess[str]:
  103. env = os.environ.copy()
  104. result = subprocess.run(
  105. [python_executable, "-m", "alembic", "upgrade", "head"],
  106. cwd=target.service_path,
  107. env=env,
  108. text=True,
  109. check=False)
  110. return result
  111. if __name__ == "__main__":
  112. raise SystemExit(main())