sync.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. from __future__ import annotations
  2. import configparser
  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, volta
  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. "VOLTA_HOME": f"{reporoot}/.devenv/bin/volta-home",
  28. "PATH": f"{venv_path}/bin:{reporoot}/.devenv/bin:{proc.base_path}",
  29. }
  30. if extra_env:
  31. env = {**env, **extra_env}
  32. procs.append(
  33. (
  34. name,
  35. cmd,
  36. subprocess.Popen(
  37. cmd,
  38. stdout=stdout,
  39. stderr=stderr,
  40. env=env,
  41. cwd=reporoot,
  42. ),
  43. )
  44. )
  45. all_good = True
  46. for name, final_cmd, p in procs:
  47. out, _ = p.communicate()
  48. if p.returncode != 0:
  49. all_good = False
  50. out_str = f"Output:\n{out.decode()}" if not verbose else ""
  51. print(
  52. f"""
  53. ❌ {name}
  54. failed command (code {p.returncode}):
  55. {shlex.join(final_cmd)}
  56. {out_str}
  57. """
  58. )
  59. else:
  60. print(f"✅ {name}")
  61. return all_good
  62. def main(context: dict[str, str]) -> int:
  63. repo = context["repo"]
  64. reporoot = context["reporoot"]
  65. # TODO: context["verbose"]
  66. verbose = os.environ.get("SENTRY_DEVENV_VERBOSE") is not None
  67. FRONTEND_ONLY = os.environ.get("SENTRY_DEVENV_FRONTEND_ONLY") is not None
  68. # venv's still needed for frontend because repo-local devenv and pre-commit
  69. # exist inside it
  70. venv_dir, python_version, requirements, editable_paths, bins = venv.get(reporoot, repo)
  71. url, sha256 = config.get_python(reporoot, python_version)
  72. print(f"ensuring {repo} venv at {venv_dir}...")
  73. venv.ensure(venv_dir, python_version, url, sha256)
  74. # TODO: move volta version into per-repo config
  75. try:
  76. volta.install(reporoot)
  77. except TypeError:
  78. # this is needed for devenv <=1.4.0,>1.2.3 to finish syncing and therefore update itself
  79. volta.install()
  80. if constants.DARWIN:
  81. repo_config = configparser.ConfigParser()
  82. repo_config.read(f"{reporoot}/devenv/config.ini")
  83. try:
  84. colima.install(
  85. repo_config["colima"]["version"],
  86. repo_config["colima"][constants.SYSTEM_MACHINE],
  87. repo_config["colima"][f"{constants.SYSTEM_MACHINE}_sha256"],
  88. reporoot,
  89. )
  90. except TypeError:
  91. # this is needed for devenv <=1.4.0,>1.2.3 to finish syncing and therefore update itself
  92. colima.install(
  93. repo_config["colima"]["version"],
  94. repo_config["colima"][constants.SYSTEM_MACHINE],
  95. repo_config["colima"][f"{constants.SYSTEM_MACHINE}_sha256"],
  96. )
  97. # TODO: move limactl version into per-repo config
  98. try:
  99. limactl.install(reporoot)
  100. except TypeError:
  101. # this is needed for devenv <=1.4.0,>1.2.3 to finish syncing and therefore update itself
  102. limactl.install()
  103. if not run_procs(
  104. repo,
  105. reporoot,
  106. venv_dir,
  107. (
  108. # TODO: devenv should provide a job runner (jobs run in parallel, tasks run sequentially)
  109. (
  110. "python dependencies (1/4)",
  111. (
  112. # upgrading pip first
  113. "pip",
  114. "install",
  115. "--constraint",
  116. "requirements-dev-frozen.txt",
  117. "pip",
  118. ),
  119. {},
  120. ),
  121. ),
  122. verbose,
  123. ):
  124. return 1
  125. if not run_procs(
  126. repo,
  127. reporoot,
  128. venv_dir,
  129. (
  130. (
  131. # Spreading out the network load by installing js,
  132. # then py in the next batch.
  133. "javascript dependencies (1/1)",
  134. (
  135. "yarn",
  136. "install",
  137. "--frozen-lockfile",
  138. "--no-progress",
  139. "--non-interactive",
  140. ),
  141. {
  142. "NODE_ENV": "development",
  143. },
  144. ),
  145. (
  146. "python dependencies (2/4)",
  147. (
  148. "pip",
  149. "uninstall",
  150. "-qqy",
  151. "djangorestframework-stubs",
  152. "django-stubs",
  153. ),
  154. {},
  155. ),
  156. ),
  157. verbose,
  158. ):
  159. return 1
  160. if not run_procs(
  161. repo,
  162. reporoot,
  163. venv_dir,
  164. (
  165. # could opt out of syncing python if FRONTEND_ONLY but only if repo-local devenv
  166. # and pre-commit were moved to inside devenv and not the sentry venv
  167. (
  168. "python dependencies (3/4)",
  169. (
  170. "pip",
  171. "install",
  172. "--constraint",
  173. "requirements-dev-frozen.txt",
  174. "-r",
  175. "requirements-dev-frozen.txt",
  176. ),
  177. {},
  178. ),
  179. ),
  180. verbose,
  181. ):
  182. return 1
  183. if not run_procs(
  184. repo,
  185. reporoot,
  186. venv_dir,
  187. (
  188. (
  189. "python dependencies (4/4)",
  190. ("python3", "-m", "tools.fast_editable", "--path", "."),
  191. {},
  192. ),
  193. ("pre-commit dependencies", ("pre-commit", "install", "--install-hooks", "-f"), {}),
  194. ),
  195. verbose,
  196. ):
  197. return 1
  198. fs.ensure_symlink("../../config/hooks/post-merge", f"{reporoot}/.git/hooks/post-merge")
  199. if not os.path.exists(f"{constants.home}/.sentry/config.yml") or not os.path.exists(
  200. f"{constants.home}/.sentry/sentry.conf.py"
  201. ):
  202. proc.run((f"{venv_dir}/bin/sentry", "init", "--dev"))
  203. # Frontend engineers don't necessarily always have devservices running and
  204. # can configure to skip them to save on local resources
  205. if FRONTEND_ONLY:
  206. print("Skipping python migrations since SENTRY_DEVENV_FRONTEND_ONLY is set.")
  207. return 0
  208. # TODO: check healthchecks for redis and postgres to short circuit this
  209. proc.run(
  210. (
  211. f"{venv_dir}/bin/{repo}",
  212. "devservices",
  213. "up",
  214. "redis",
  215. "postgres",
  216. ),
  217. pathprepend=f"{reporoot}/.devenv/bin",
  218. exit=True,
  219. )
  220. if not run_procs(
  221. repo,
  222. reporoot,
  223. venv_dir,
  224. (
  225. (
  226. "python migrations",
  227. ("make", "apply-migrations"),
  228. {},
  229. ),
  230. ),
  231. verbose,
  232. ):
  233. return 1
  234. # faster prerequisite check than starting up sentry and running createuser idempotently
  235. stdout = proc.run(
  236. (
  237. "docker",
  238. "exec",
  239. "sentry_postgres",
  240. "psql",
  241. "sentry",
  242. "postgres",
  243. "-t",
  244. "-c",
  245. "select exists (select from auth_user where email = 'admin@sentry.io')",
  246. ),
  247. stdout=True,
  248. )
  249. if stdout != "t":
  250. proc.run(
  251. (
  252. f"{venv_dir}/bin/sentry",
  253. "createuser",
  254. "--superuser",
  255. "--email",
  256. "admin@sentry.io",
  257. "--password",
  258. "admin",
  259. "--no-input",
  260. )
  261. )
  262. return 0