sync.py 8.4 KB

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