sync.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. from __future__ import annotations
  2. import importlib.metadata
  3. import os
  4. import shlex
  5. import subprocess
  6. from devenv import constants
  7. from devenv.lib import colima, config, fs, limactl, proc, venv
  8. # TODO: need to replace this with a nicer process executor in devenv.lib
  9. def run_procs(
  10. repo: str,
  11. reporoot: str,
  12. venv_path: str,
  13. _procs: tuple[tuple[str, tuple[str, ...], dict[str, str]], ...],
  14. verbose: bool = False,
  15. ) -> bool:
  16. procs: list[tuple[str, tuple[str, ...], subprocess.Popen[bytes]]] = []
  17. stdout = subprocess.PIPE if not verbose else None
  18. stderr = subprocess.STDOUT if not verbose else None
  19. for name, cmd, extra_env in _procs:
  20. print(f"⏳ {name}")
  21. if constants.DEBUG:
  22. proc.xtrace(cmd)
  23. env = {
  24. **constants.user_environ,
  25. **proc.base_env,
  26. "VIRTUAL_ENV": venv_path,
  27. "PATH": f"{venv_path}/bin:{reporoot}/.devenv/bin:{proc.base_path}",
  28. }
  29. if extra_env:
  30. env = {**env, **extra_env}
  31. procs.append(
  32. (
  33. name,
  34. cmd,
  35. subprocess.Popen(
  36. cmd,
  37. stdout=stdout,
  38. stderr=stderr,
  39. env=env,
  40. cwd=reporoot,
  41. ),
  42. )
  43. )
  44. all_good = True
  45. for name, final_cmd, p in procs:
  46. out, _ = p.communicate()
  47. if p.returncode != 0:
  48. all_good = False
  49. out_str = f"Output:\n{out.decode()}" if not verbose else ""
  50. print(
  51. f"""
  52. ❌ {name}
  53. failed command (code {p.returncode}):
  54. {shlex.join(final_cmd)}
  55. {out_str}
  56. """
  57. )
  58. else:
  59. print(f"✅ {name}")
  60. return all_good
  61. # Temporary, see https://github.com/getsentry/sentry/pull/78881
  62. def check_minimum_version(minimum_version: str) -> bool:
  63. version = importlib.metadata.version("sentry-devenv")
  64. parsed_version = tuple(map(int, version.split(".")))
  65. parsed_minimum_version = tuple(map(int, minimum_version.split(".")))
  66. return parsed_version >= parsed_minimum_version
  67. def main(context: dict[str, str]) -> int:
  68. minimum_version = "1.13.0"
  69. if not check_minimum_version(minimum_version):
  70. raise SystemExit(
  71. f"""
  72. Hi! To reduce potential breakage we've defined a minimum
  73. devenv version ({minimum_version}) to run sync.
  74. Please run the following to update your global devenv:
  75. devenv update
  76. Then, use it to run sync this one time.
  77. {constants.root}/bin/devenv sync
  78. """
  79. )
  80. repo = context["repo"]
  81. reporoot = context["reporoot"]
  82. repo_config = config.get_config(f"{reporoot}/devenv/config.ini")
  83. # TODO: context["verbose"]
  84. verbose = os.environ.get("SENTRY_DEVENV_VERBOSE") is not None
  85. FRONTEND_ONLY = os.environ.get("SENTRY_DEVENV_FRONTEND_ONLY") is not None
  86. USE_NEW_DEVSERVICES = os.environ.get("USE_NEW_DEVSERVICES") == "1"
  87. from devenv.lib import node
  88. node.install(
  89. repo_config["node"]["version"],
  90. repo_config["node"][constants.SYSTEM_MACHINE],
  91. repo_config["node"][f"{constants.SYSTEM_MACHINE}_sha256"],
  92. reporoot,
  93. )
  94. node.install_yarn(repo_config["node"]["yarn_version"], reporoot)
  95. # no more imports from devenv past this point! if the venv is recreated
  96. # then we won't have access to devenv libs until it gets reinstalled
  97. # venv's still needed for frontend because repo-local devenv and pre-commit
  98. # exist inside it
  99. venv_dir, python_version, requirements, editable_paths, bins = venv.get(reporoot, repo)
  100. url, sha256 = config.get_python(reporoot, python_version)
  101. print(f"ensuring {repo} venv at {venv_dir}...")
  102. venv.ensure(venv_dir, python_version, url, sha256)
  103. if constants.DARWIN:
  104. if check_minimum_version("1.14.2"):
  105. # `devenv update`ing to >=1.14.0 will install global colima
  106. # so if it's there, uninstall the repo local stuff
  107. if os.path.exists(f"{constants.root}/bin/colima"):
  108. binroot = f"{reporoot}/.devenv/bin"
  109. colima.uninstall(binroot)
  110. limactl.uninstall(binroot)
  111. else:
  112. colima.install(
  113. repo_config["colima"]["version"],
  114. repo_config["colima"][constants.SYSTEM_MACHINE],
  115. repo_config["colima"][f"{constants.SYSTEM_MACHINE}_sha256"],
  116. reporoot,
  117. )
  118. limactl.install(
  119. repo_config["lima"]["version"],
  120. repo_config["lima"][constants.SYSTEM_MACHINE],
  121. repo_config["lima"][f"{constants.SYSTEM_MACHINE}_sha256"],
  122. reporoot,
  123. )
  124. if not run_procs(
  125. repo,
  126. reporoot,
  127. venv_dir,
  128. (
  129. # TODO: devenv should provide a job runner (jobs run in parallel, tasks run sequentially)
  130. (
  131. "python dependencies (1/4)",
  132. (
  133. # upgrading pip first
  134. "pip",
  135. "install",
  136. "--constraint",
  137. "requirements-dev-frozen.txt",
  138. "pip",
  139. ),
  140. {},
  141. ),
  142. ),
  143. verbose,
  144. ):
  145. return 1
  146. if not run_procs(
  147. repo,
  148. reporoot,
  149. venv_dir,
  150. (
  151. (
  152. # Spreading out the network load by installing js,
  153. # then py in the next batch.
  154. "javascript dependencies (1/1)",
  155. (
  156. "yarn",
  157. "install",
  158. "--frozen-lockfile",
  159. "--no-progress",
  160. "--non-interactive",
  161. ),
  162. {
  163. "NODE_ENV": "development",
  164. },
  165. ),
  166. (
  167. "python dependencies (2/4)",
  168. (
  169. "pip",
  170. "uninstall",
  171. "-qqy",
  172. "djangorestframework-stubs",
  173. "django-stubs",
  174. ),
  175. {},
  176. ),
  177. ),
  178. verbose,
  179. ):
  180. return 1
  181. if not run_procs(
  182. repo,
  183. reporoot,
  184. venv_dir,
  185. (
  186. # could opt out of syncing python if FRONTEND_ONLY but only if repo-local devenv
  187. # and pre-commit were moved to inside devenv and not the sentry venv
  188. (
  189. "python dependencies (3/4)",
  190. (
  191. "pip",
  192. "install",
  193. "--constraint",
  194. "requirements-dev-frozen.txt",
  195. "-r",
  196. "requirements-dev-frozen.txt",
  197. ),
  198. {},
  199. ),
  200. ),
  201. verbose,
  202. ):
  203. return 1
  204. if not run_procs(
  205. repo,
  206. reporoot,
  207. venv_dir,
  208. (
  209. (
  210. "python dependencies (4/4)",
  211. ("python3", "-m", "tools.fast_editable", "--path", "."),
  212. {},
  213. ),
  214. ("pre-commit dependencies", ("pre-commit", "install", "--install-hooks", "-f"), {}),
  215. ),
  216. verbose,
  217. ):
  218. return 1
  219. fs.ensure_symlink("../../config/hooks/post-merge", f"{reporoot}/.git/hooks/post-merge")
  220. if not os.path.exists(f"{constants.home}/.sentry/config.yml") or not os.path.exists(
  221. f"{constants.home}/.sentry/sentry.conf.py"
  222. ):
  223. proc.run((f"{venv_dir}/bin/sentry", "init", "--dev"))
  224. # Frontend engineers don't necessarily always have devservices running and
  225. # can configure to skip them to save on local resources
  226. if FRONTEND_ONLY:
  227. print("Skipping python migrations since SENTRY_DEVENV_FRONTEND_ONLY is set.")
  228. return 0
  229. if USE_NEW_DEVSERVICES:
  230. # Ensure old sentry devservices is not being used, otherwise ports will conflict
  231. proc.run(
  232. (
  233. f"{venv_dir}/bin/{repo}",
  234. "devservices",
  235. "down",
  236. ),
  237. pathprepend=f"{reporoot}/.devenv/bin",
  238. exit=True,
  239. )
  240. proc.run(
  241. (f"{venv_dir}/bin/devservices", "up", "--mode", "migrations"),
  242. pathprepend=f"{reporoot}/.devenv/bin",
  243. exit=True,
  244. )
  245. else:
  246. # TODO: check healthchecks for redis and postgres to short circuit this
  247. proc.run(
  248. (
  249. f"{venv_dir}/bin/{repo}",
  250. "devservices",
  251. "up",
  252. "redis",
  253. "postgres",
  254. ),
  255. pathprepend=f"{reporoot}/.devenv/bin",
  256. exit=True,
  257. )
  258. if not run_procs(
  259. repo,
  260. reporoot,
  261. venv_dir,
  262. (
  263. (
  264. "python migrations",
  265. ("make", "apply-migrations"),
  266. {},
  267. ),
  268. ),
  269. verbose,
  270. ):
  271. return 1
  272. postgres_container = (
  273. "sentry_postgres" if os.environ.get("USE_NEW_DEVSERVICES") != "1" else "sentry-postgres-1"
  274. )
  275. # faster prerequisite check than starting up sentry and running createuser idempotently
  276. stdout = proc.run(
  277. (
  278. "docker",
  279. "exec",
  280. postgres_container,
  281. "psql",
  282. "sentry",
  283. "postgres",
  284. "-t",
  285. "-c",
  286. "select exists (select from auth_user where email = 'admin@sentry.io')",
  287. ),
  288. stdout=True,
  289. )
  290. if stdout != "t":
  291. proc.run(
  292. (
  293. f"{venv_dir}/bin/sentry",
  294. "createuser",
  295. "--superuser",
  296. "--email",
  297. "admin@sentry.io",
  298. "--password",
  299. "admin",
  300. "--no-input",
  301. )
  302. )
  303. return 0