migrate_all.py 3.6 KB

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