@vaharoni/devops 1.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/README.md +51 -0
  2. package/dist/app-support/crypto/index.d.ts +15 -0
  3. package/dist/app-support/crypto/index.d.ts.map +1 -0
  4. package/dist/app-support/crypto/index.js +30 -0
  5. package/dist/app-support/crypto/internal-token.d.ts +20 -0
  6. package/dist/app-support/crypto/internal-token.d.ts.map +1 -0
  7. package/dist/app-support/crypto/internal-token.js +42 -0
  8. package/dist/app-support/crypto/internal-token.spec.d.ts +2 -0
  9. package/dist/app-support/crypto/internal-token.spec.d.ts.map +1 -0
  10. package/dist/app-support/crypto/internal-token.spec.js +45 -0
  11. package/dist/app-support/crypto/secret.d.ts +3 -0
  12. package/dist/app-support/crypto/secret.d.ts.map +1 -0
  13. package/dist/app-support/crypto/secret.js +12 -0
  14. package/dist/app-support/crypto/secret.spec.d.ts +2 -0
  15. package/dist/app-support/crypto/secret.spec.d.ts.map +1 -0
  16. package/dist/app-support/crypto/secret.spec.js +15 -0
  17. package/dist/app-support/discovery/dev-discovery-loader.d.ts +2 -0
  18. package/dist/app-support/discovery/dev-discovery-loader.d.ts.map +1 -0
  19. package/dist/app-support/discovery/dev-discovery-loader.js +30 -0
  20. package/dist/app-support/discovery/service-endpoint.d.ts +2 -0
  21. package/dist/app-support/discovery/service-endpoint.d.ts.map +1 -0
  22. package/dist/app-support/discovery/service-endpoint.js +10 -0
  23. package/dist/cli/affected.d.ts +11 -0
  24. package/dist/cli/affected.d.ts.map +1 -0
  25. package/dist/cli/affected.js +103 -0
  26. package/dist/cli/common.d.ts +89 -0
  27. package/dist/cli/common.d.ts.map +1 -0
  28. package/dist/cli/common.js +236 -0
  29. package/dist/cli/common.spec.d.ts +2 -0
  30. package/dist/cli/common.spec.d.ts.map +1 -0
  31. package/dist/cli/common.spec.js +64 -0
  32. package/dist/cli/console.d.ts +11 -0
  33. package/dist/cli/console.d.ts.map +1 -0
  34. package/dist/cli/console.js +35 -0
  35. package/dist/cli/constant.d.ts +11 -0
  36. package/dist/cli/constant.d.ts.map +1 -0
  37. package/dist/cli/constant.js +22 -0
  38. package/dist/cli/db.d.ts +11 -0
  39. package/dist/cli/db.d.ts.map +1 -0
  40. package/dist/cli/db.js +119 -0
  41. package/dist/cli/dml.d.ts +11 -0
  42. package/dist/cli/dml.d.ts.map +1 -0
  43. package/dist/cli/dml.js +116 -0
  44. package/dist/cli/env.d.ts +11 -0
  45. package/dist/cli/env.d.ts.map +1 -0
  46. package/dist/cli/env.js +67 -0
  47. package/dist/cli/exec.d.ts +11 -0
  48. package/dist/cli/exec.d.ts.map +1 -0
  49. package/dist/cli/exec.js +50 -0
  50. package/dist/cli/image.d.ts +11 -0
  51. package/dist/cli/image.d.ts.map +1 -0
  52. package/dist/cli/image.js +140 -0
  53. package/dist/cli/init.d.ts +11 -0
  54. package/dist/cli/init.d.ts.map +1 -0
  55. package/dist/cli/init.js +66 -0
  56. package/dist/cli/internal-curl.d.ts +11 -0
  57. package/dist/cli/internal-curl.d.ts.map +1 -0
  58. package/dist/cli/internal-curl.js +43 -0
  59. package/dist/cli/job.d.ts +11 -0
  60. package/dist/cli/job.d.ts.map +1 -0
  61. package/dist/cli/job.js +67 -0
  62. package/dist/cli/jwt.d.ts +11 -0
  63. package/dist/cli/jwt.d.ts.map +1 -0
  64. package/dist/cli/jwt.js +27 -0
  65. package/dist/cli/namespace.d.ts +11 -0
  66. package/dist/cli/namespace.d.ts.map +1 -0
  67. package/dist/cli/namespace.js +70 -0
  68. package/dist/cli/prep-build.d.ts +11 -0
  69. package/dist/cli/prep-build.d.ts.map +1 -0
  70. package/dist/cli/prep-build.js +82 -0
  71. package/dist/cli/prisma.d.ts +11 -0
  72. package/dist/cli/prisma.d.ts.map +1 -0
  73. package/dist/cli/prisma.js +25 -0
  74. package/dist/cli/redis.d.ts +11 -0
  75. package/dist/cli/redis.d.ts.map +1 -0
  76. package/dist/cli/redis.js +76 -0
  77. package/dist/cli/registry.d.ts +11 -0
  78. package/dist/cli/registry.d.ts.map +1 -0
  79. package/dist/cli/registry.js +58 -0
  80. package/dist/cli/run-many.d.ts +11 -0
  81. package/dist/cli/run-many.d.ts.map +1 -0
  82. package/dist/cli/run-many.js +50 -0
  83. package/dist/cli/run.d.ts +11 -0
  84. package/dist/cli/run.d.ts.map +1 -0
  85. package/dist/cli/run.js +37 -0
  86. package/dist/cli/template.d.ts +11 -0
  87. package/dist/cli/template.d.ts.map +1 -0
  88. package/dist/cli/template.js +123 -0
  89. package/dist/cli/test.d.ts +11 -0
  90. package/dist/cli/test.d.ts.map +1 -0
  91. package/dist/cli/test.js +28 -0
  92. package/dist/devops.d.ts +3 -0
  93. package/dist/devops.d.ts.map +1 -0
  94. package/dist/devops.js +103 -0
  95. package/dist/index.d.ts +4 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +3 -0
  98. package/dist/libs/affected-entities.d.ts +15 -0
  99. package/dist/libs/affected-entities.d.ts.map +1 -0
  100. package/dist/libs/affected-entities.js +52 -0
  101. package/dist/libs/config.d.ts +6 -0
  102. package/dist/libs/config.d.ts.map +1 -0
  103. package/dist/libs/config.js +98 -0
  104. package/dist/libs/dependencies.d.ts +19 -0
  105. package/dist/libs/dependencies.d.ts.map +1 -0
  106. package/dist/libs/dependencies.js +62 -0
  107. package/dist/libs/dependencies.spec.d.ts +2 -0
  108. package/dist/libs/dependencies.spec.d.ts.map +1 -0
  109. package/dist/libs/dependencies.spec.js +21 -0
  110. package/dist/libs/digital-ocean/container-reg.d.ts +6 -0
  111. package/dist/libs/digital-ocean/container-reg.d.ts.map +1 -0
  112. package/dist/libs/digital-ocean/container-reg.js +69 -0
  113. package/dist/libs/discovery/dependencies.d.ts +19 -0
  114. package/dist/libs/discovery/dependencies.d.ts.map +1 -0
  115. package/dist/libs/discovery/dependencies.js +62 -0
  116. package/dist/libs/discovery/dependencies.spec.d.ts +2 -0
  117. package/dist/libs/discovery/dependencies.spec.d.ts.map +1 -0
  118. package/dist/libs/discovery/dependencies.spec.js +21 -0
  119. package/dist/libs/discovery/images.d.ts +5 -0
  120. package/dist/libs/discovery/images.d.ts.map +1 -0
  121. package/dist/libs/discovery/images.js +45 -0
  122. package/dist/libs/discovery/index.d.ts +5 -0
  123. package/dist/libs/discovery/index.d.ts.map +1 -0
  124. package/dist/libs/discovery/index.js +55 -0
  125. package/dist/libs/discovery/package-json-processor.d.ts +3 -0
  126. package/dist/libs/discovery/package-json-processor.d.ts.map +1 -0
  127. package/dist/libs/discovery/package-json-processor.js +34 -0
  128. package/dist/libs/discovery/process-common.d.ts +25 -0
  129. package/dist/libs/discovery/process-common.d.ts.map +1 -0
  130. package/dist/libs/discovery/process-common.js +40 -0
  131. package/dist/libs/discovery/process-package-json.d.ts +3 -0
  132. package/dist/libs/discovery/process-package-json.d.ts.map +1 -0
  133. package/dist/libs/discovery/process-package-json.js +34 -0
  134. package/dist/libs/discovery/process-pyproject-toml.d.ts +3 -0
  135. package/dist/libs/discovery/process-pyproject-toml.d.ts.map +1 -0
  136. package/dist/libs/discovery/process-pyproject-toml.js +36 -0
  137. package/dist/libs/discovery/pyproject-toml-processor.d.ts +3 -0
  138. package/dist/libs/discovery/pyproject-toml-processor.d.ts.map +1 -0
  139. package/dist/libs/discovery/pyproject-toml-processor.js +39 -0
  140. package/dist/libs/git-helpers.d.ts +8 -0
  141. package/dist/libs/git-helpers.d.ts.map +1 -0
  142. package/dist/libs/git-helpers.js +20 -0
  143. package/dist/libs/hetzner/reg-secret.d.ts +3 -0
  144. package/dist/libs/hetzner/reg-secret.d.ts.map +1 -0
  145. package/dist/libs/hetzner/reg-secret.js +39 -0
  146. package/dist/libs/k8s-constants.d.ts +12 -0
  147. package/dist/libs/k8s-constants.d.ts.map +1 -0
  148. package/dist/libs/k8s-constants.js +66 -0
  149. package/dist/libs/k8s-db.d.ts +18 -0
  150. package/dist/libs/k8s-db.d.ts.map +1 -0
  151. package/dist/libs/k8s-db.js +73 -0
  152. package/dist/libs/k8s-generate.d.ts +17 -0
  153. package/dist/libs/k8s-generate.d.ts.map +1 -0
  154. package/dist/libs/k8s-generate.js +179 -0
  155. package/dist/libs/k8s-helpers.d.ts +11 -0
  156. package/dist/libs/k8s-helpers.d.ts.map +1 -0
  157. package/dist/libs/k8s-helpers.js +42 -0
  158. package/dist/libs/k8s-image-config.d.ts +8 -0
  159. package/dist/libs/k8s-image-config.d.ts.map +1 -0
  160. package/dist/libs/k8s-image-config.js +113 -0
  161. package/dist/libs/k8s-job-waiter.d.ts +8 -0
  162. package/dist/libs/k8s-job-waiter.d.ts.map +1 -0
  163. package/dist/libs/k8s-job-waiter.js +84 -0
  164. package/dist/libs/k8s-namespace.d.ts +7 -0
  165. package/dist/libs/k8s-namespace.d.ts.map +1 -0
  166. package/dist/libs/k8s-namespace.js +27 -0
  167. package/dist/libs/k8s-redis.d.ts +6 -0
  168. package/dist/libs/k8s-redis.d.ts.map +1 -0
  169. package/dist/libs/k8s-redis.js +31 -0
  170. package/dist/libs/k8s-secrets-manager.d.ts +5 -0
  171. package/dist/libs/k8s-secrets-manager.d.ts.map +1 -0
  172. package/dist/libs/k8s-secrets-manager.js +61 -0
  173. package/dist/libs/validate-env.d.ts +56 -0
  174. package/dist/libs/validate-env.d.ts.map +1 -0
  175. package/dist/libs/validate-env.js +214 -0
  176. package/dist/libs/validate-env.spec.d.ts +2 -0
  177. package/dist/libs/validate-env.spec.d.ts.map +1 -0
  178. package/dist/libs/validate-env.spec.js +168 -0
  179. package/dist/libs/workspace-discovery.d.ts +2 -0
  180. package/dist/libs/workspace-discovery.d.ts.map +1 -0
  181. package/dist/libs/workspace-discovery.js +75 -0
  182. package/dist/test.d.ts +2 -0
  183. package/dist/test.d.ts.map +1 -0
  184. package/dist/test.js +1 -0
  185. package/dist/types/index.d.ts +925 -0
  186. package/dist/types/index.d.ts.map +1 -0
  187. package/dist/types/index.js +79 -0
  188. package/package.json +55 -0
  189. package/src/app-support/crypto/index.ts +31 -0
  190. package/src/app-support/crypto/internal-token.spec.ts +53 -0
  191. package/src/app-support/crypto/internal-token.ts +82 -0
  192. package/src/app-support/crypto/secret.spec.ts +18 -0
  193. package/src/app-support/crypto/secret.ts +13 -0
  194. package/src/app-support/discovery/dev-discovery-loader.ts +35 -0
  195. package/src/app-support/discovery/service-endpoint.ts +12 -0
  196. package/src/cli/affected.ts +116 -0
  197. package/src/cli/common.spec.ts +78 -0
  198. package/src/cli/common.ts +323 -0
  199. package/src/cli/console.ts +46 -0
  200. package/src/cli/constant.ts +25 -0
  201. package/src/cli/db.ts +133 -0
  202. package/src/cli/dml.ts +126 -0
  203. package/src/cli/env.ts +87 -0
  204. package/src/cli/exec.sh +21 -0
  205. package/src/cli/exec.ts +57 -0
  206. package/src/cli/image.ts +197 -0
  207. package/src/cli/init.ts +75 -0
  208. package/src/cli/internal-curl.ts +48 -0
  209. package/src/cli/job.ts +80 -0
  210. package/src/cli/jwt.ts +32 -0
  211. package/src/cli/namespace.ts +78 -0
  212. package/src/cli/prep-build.ts +96 -0
  213. package/src/cli/prisma.ts +33 -0
  214. package/src/cli/redis.ts +83 -0
  215. package/src/cli/registry.ts +76 -0
  216. package/src/cli/run-many.ts +61 -0
  217. package/src/cli/run.ts +46 -0
  218. package/src/cli/template.ts +169 -0
  219. package/src/cli/test.ts +30 -0
  220. package/src/devops.ts +119 -0
  221. package/src/index.ts +3 -0
  222. package/src/libs/affected-entities.ts +71 -0
  223. package/src/libs/config.ts +117 -0
  224. package/src/libs/digital-ocean/container-reg.ts +81 -0
  225. package/src/libs/discovery/dependencies.spec.ts +25 -0
  226. package/src/libs/discovery/dependencies.ts +73 -0
  227. package/src/libs/discovery/images.ts +57 -0
  228. package/src/libs/discovery/index.ts +60 -0
  229. package/src/libs/discovery/process-common.ts +55 -0
  230. package/src/libs/discovery/process-package-json.ts +47 -0
  231. package/src/libs/discovery/process-pyproject-toml.ts +43 -0
  232. package/src/libs/git-helpers.ts +32 -0
  233. package/src/libs/hetzner/reg-secret.ts +54 -0
  234. package/src/libs/k8s-constants.ts +83 -0
  235. package/src/libs/k8s-db.ts +83 -0
  236. package/src/libs/k8s-generate.ts +211 -0
  237. package/src/libs/k8s-helpers.ts +59 -0
  238. package/src/libs/k8s-image-config.ts +165 -0
  239. package/src/libs/k8s-job-waiter.ts +124 -0
  240. package/src/libs/k8s-namespace.ts +41 -0
  241. package/src/libs/k8s-redis.ts +31 -0
  242. package/src/libs/k8s-secrets-manager.ts +79 -0
  243. package/src/libs/validate-env.spec.ts +223 -0
  244. package/src/libs/validate-env.ts +266 -0
  245. package/src/target-templates/.devops/config/constants.yaml +17 -0
  246. package/src/target-templates/.devops/config/images.yaml +88 -0
  247. package/src/target-templates/.devops/docker-images/common/docker-common.sh +23 -0
  248. package/src/target-templates/.devops/docker-images/node-services/node-exec.sh +8 -0
  249. package/src/target-templates/.devops/docker-images/node-services/node-run.sh +8 -0
  250. package/src/target-templates/.devops/docker-images/node-services.Dockerfile +34 -0
  251. package/src/target-templates/.devops/docker-images/python-services/python-exec.sh +8 -0
  252. package/src/target-templates/.devops/docker-images/python-services/python-run.sh +8 -0
  253. package/src/target-templates/.devops/docker-images/python-services.Dockerfile +29 -0
  254. package/src/target-templates/.devops/env.example.yaml +23 -0
  255. package/src/target-templates/.devops/infra/hetzner/abandoned/harbor-values.yaml +30 -0
  256. package/src/target-templates/.devops/infra/hetzner/abandoned/hcloud-config.yaml +134 -0
  257. package/src/target-templates/.devops/infra/hetzner/cert-manager.yaml +25 -0
  258. package/src/target-templates/.devops/infra/hetzner/harbor-cert.yaml +13 -0
  259. package/src/target-templates/.devops/infra/hetzner/harbor-values.yaml +76 -0
  260. package/src/target-templates/.devops/infra/hetzner/hcloud-config.yaml +113 -0
  261. package/src/target-templates/.devops/infra/hetzner/ingress-nginx-annotations.yaml +49 -0
  262. package/src/target-templates/.devops/infra/hetzner/ingress-nginx-configmap.yaml +8 -0
  263. package/src/target-templates/.devops/infra/hetzner/retain-storage-class.yaml +8 -0
  264. package/src/target-templates/.devops/infra/monitoring-ingress.yaml +62 -0
  265. package/src/target-templates/.devops/infra/stackgres-ui-ingress.yaml +35 -0
  266. package/src/target-templates/.devops/infra/test.yaml +60 -0
  267. package/src/target-templates/.devops/manifests/_index.yaml +21 -0
  268. package/src/target-templates/.devops/manifests/cron-jobs.yaml.hb +55 -0
  269. package/src/target-templates/.devops/manifests/db-migrate-job.yaml.hb +42 -0
  270. package/src/target-templates/.devops/manifests/deployment-debug.yaml.hb +44 -0
  271. package/src/target-templates/.devops/manifests/deployment-process.yaml.hb +47 -0
  272. package/src/target-templates/.devops/manifests/deployment-web.yaml.hb +53 -0
  273. package/src/target-templates/.devops/manifests/ingress.yaml.hb +21 -0
  274. package/src/target-templates/.devops/manifests/prefect.yaml.hb +62 -0
  275. package/src/target-templates/.devops/manifests/service.yaml.hb +15 -0
  276. package/src/target-templates/.devops/milvus/production/milvus-values.yaml +2 -0
  277. package/src/target-templates/.devops/milvus/staging/milvus-values.yaml +2 -0
  278. package/src/target-templates/.devops/postgres/DailyOperatorRestart.yaml +54 -0
  279. package/src/target-templates/.devops/postgres/production/cluster/PodDisruptionBudget.yaml +27 -0
  280. package/src/target-templates/.devops/postgres/production/cluster/SGCluster.yaml +47 -0
  281. package/src/target-templates/.devops/postgres/production/cluster/StackGres-alerts.yaml +191 -0
  282. package/src/target-templates/.devops/postgres/production/configurations/06-SGDistributedLogs.yaml +11 -0
  283. package/src/target-templates/.devops/postgres/production/configurations/07-SGObjectStorage.yaml +18 -0
  284. package/src/target-templates/.devops/postgres/production/configurations/08-SGScript.yaml +12 -0
  285. package/src/target-templates/.devops/postgres/staging/cluster/SGCluster.yaml +42 -0
  286. package/src/target-templates/.devops/postgres/staging/configurations/07-SGObjectStorage.yaml +18 -0
  287. package/src/target-templates/.devops/postgres/staging/configurations/08-SGScript.yaml +12 -0
  288. package/src/target-templates/.devops/prefect/production/prefect-values.yaml +14 -0
  289. package/src/target-templates/.devops/prefect/staging/prefect-values.yaml +14 -0
  290. package/src/target-templates/.devops/redis/production/redis-values.yaml +20 -0
  291. package/src/target-templates/.devops/redis/staging/redis-values.yaml +8 -0
  292. package/src/target-templates/.envrc +5 -0
  293. package/src/target-templates/.github/actions/build-image@v1/action.yaml +86 -0
  294. package/src/target-templates/.github/actions/connect-to-digital-ocean@v1/action.yaml +29 -0
  295. package/src/target-templates/.github/actions/connect-to-hetzner@v1/action.yaml +31 -0
  296. package/src/target-templates/.github/actions/connect-to-infra@v1/action.yaml +46 -0
  297. package/src/target-templates/.github/actions/db-migrate@v1/action.yaml +23 -0
  298. package/src/target-templates/.github/actions/deploy-image@v1/action.yaml +33 -0
  299. package/src/target-templates/.github/actions/setup-prereq@v1/action.yaml +29 -0
  300. package/src/target-templates/.github/workflows/k8s-build.yaml +84 -0
  301. package/src/target-templates/applications/example-data-pipeline/pyproject.toml +14 -0
  302. package/src/target-templates/applications/example-data-pipeline/src/example_data_pipeline/main.py +38 -0
  303. package/src/target-templates/applications/example-node/index.ts +30 -0
  304. package/src/target-templates/applications/example-node/package.json +26 -0
  305. package/src/target-templates/applications/example-node/tsconfig.json +3 -0
  306. package/src/target-templates/applications/example-python/pyproject.toml +20 -0
  307. package/src/target-templates/applications/example-python/src/example_python/__init__.py +0 -0
  308. package/src/target-templates/applications/example-python/src/example_python/main.py +13 -0
  309. package/src/target-templates/applications/example-python/src/example_python/scripts.py +17 -0
  310. package/src/target-templates/applications/example-python/tests/__init__.py +0 -0
  311. package/src/target-templates/applications/jobs/README.md +68 -0
  312. package/src/target-templates/applications/jobs/index.ts +1 -0
  313. package/src/target-templates/applications/jobs/package.json +30 -0
  314. package/src/target-templates/applications/jobs/tsconfig.json +3 -0
  315. package/src/target-templates/config/.env.development +1 -0
  316. package/src/target-templates/config/.env.global +4 -0
  317. package/src/target-templates/config/.env.test +1 -0
  318. package/src/target-templates/db/db/__init__.py +0 -0
  319. package/src/target-templates/db/db/db_client_test.py +46 -0
  320. package/src/target-templates/db/db-client-test.ts +140 -0
  321. package/src/target-templates/db/db-client.ts +19 -0
  322. package/src/target-templates/db/env.yaml +4 -0
  323. package/src/target-templates/db/package.json +17 -0
  324. package/src/target-templates/db/prisma/schema.prisma +24 -0
  325. package/src/target-templates/db/prisma-setup-vitest.ts +27 -0
  326. package/src/target-templates/db/pyproject.toml +14 -0
  327. package/src/target-templates/db/tsconfig.json +3 -0
  328. package/src/target-templates/devops +3 -0
  329. package/src/target-templates/devopspy +3 -0
  330. package/src/target-templates/dml/package.json +7 -0
  331. package/src/target-templates/dml/tsconfig.json +3 -0
  332. package/src/target-templates/libs/example-node-lib/bun.lock +27 -0
  333. package/src/target-templates/libs/example-node-lib/index.ts +3 -0
  334. package/src/target-templates/libs/example-node-lib/package.json +12 -0
  335. package/src/target-templates/libs/example-node-lib/tsconfig.json +3 -0
  336. package/src/target-templates/libs/example-python-lib/pyproject.toml +11 -0
  337. package/src/target-templates/libs/example-python-lib/src/example_python_lib/__init__.py +2 -0
  338. package/src/target-templates/pyproject.toml +19 -0
  339. package/src/target-templates/tmp/.gitkeep +0 -0
  340. package/src/target-templates/tsconfig.json +27 -0
  341. package/src/test.ts +0 -0
  342. package/src/types/index.ts +173 -0
@@ -0,0 +1,165 @@
1
+ import { CommandExecutor } from "../cli/common";
2
+ import { getWorkspace } from "./discovery";
3
+ import { imageConfigMap } from "./k8s-constants";
4
+ import { kubectlCommand, upsertConfigMapCommand } from "./k8s-helpers";
5
+
6
+ //= config map
7
+
8
+ type ImageConfigMap = {
9
+ version?: string;
10
+ scale?: string;
11
+ }
12
+
13
+ function updateImageConfigMap(monorepoEnv: string, image: string, data = {}) {
14
+ const imageConfigMapName = imageConfigMap(image);
15
+ return new CommandExecutor(
16
+ upsertConfigMapCommand(monorepoEnv, imageConfigMapName, data)
17
+ ).exec();
18
+ }
19
+
20
+ function getImageConfigMap(monorepoEnv: string, image: string): ImageConfigMap {
21
+ const imageConfigMapName = imageConfigMap(image);
22
+ const { statusCode, stdout } = new CommandExecutor(
23
+ kubectlCommand(
24
+ `get configmap ${imageConfigMapName} -o jsonpath='{.data}'`,
25
+ { monorepoEnv }
26
+ ),
27
+ { quiet: true }
28
+ ).exec({ asObject: true });
29
+ if (statusCode !== 0 || !stdout) return {};
30
+ try {
31
+ return JSON.parse(stdout);
32
+ } catch {
33
+ console.error(
34
+ `Error parsing config map data for ${image}. Received: ${stdout}`
35
+ );
36
+ process.exit(1);
37
+ }
38
+ }
39
+
40
+ function deserializeImageConfigMapKey<T>(monorepoEnv: string, image: string, key: keyof ImageConfigMap): Record<string, T> {
41
+ const value = getImageConfigMap(monorepoEnv, image)[key];
42
+ if (!value) return {};
43
+ try {
44
+ return JSON.parse(value);
45
+ }
46
+ catch {
47
+ console.error(`Error parsing config map data for ${image} for key ${key}. Received: ${value}`);
48
+ process.exit(1);
49
+ }
50
+ }
51
+
52
+ // = Version
53
+
54
+ export function getImageVersion(monorepoEnv: string, image: string) {
55
+ const data = getImageConfigMap(monorepoEnv, image);
56
+ return data.version;
57
+ }
58
+
59
+ export function setImageVersion(
60
+ monorepoEnv: string,
61
+ image: string,
62
+ version: string
63
+ ) {
64
+ const data = getImageConfigMap(monorepoEnv, image);
65
+ return updateImageConfigMap(monorepoEnv, image, { ...data, version });
66
+ }
67
+
68
+ export function deleteImageVersion(monorepoEnv: string, image: string) {
69
+ const { version, ...rest } = getImageConfigMap(monorepoEnv, image);
70
+ return updateImageConfigMap(monorepoEnv, image, rest);
71
+ }
72
+
73
+ // = Scale
74
+
75
+ function setK8sScale(
76
+ monorepoEnv: string,
77
+ workspaceName: string,
78
+ replicaCount: number
79
+ ) {
80
+ const workspaceData = getWorkspace(workspaceName);
81
+ const serviceName = workspaceData.packageDataEntries.find(x => x.deployment?.service_name)?.deployment?.service_name;
82
+ if (!serviceName) {
83
+ console.error(
84
+ `Workspace ${workspaceName} must have a service_name defined in its deployment key in package.json. Skipping.`
85
+ );
86
+ return false;
87
+ }
88
+ new CommandExecutor(
89
+ kubectlCommand(
90
+ `scale deployment ${workspaceName} --replicas=${replicaCount}`,
91
+ { monorepoEnv }
92
+ )
93
+ ).exec();
94
+ return true;
95
+ }
96
+
97
+ // Returns the old version prior to setting
98
+ export function setWorkspaceScale(
99
+ monorepoEnv: string,
100
+ image: string,
101
+ workspaceName: string,
102
+ replicaCount: number
103
+ ) {
104
+ const workspaceData = getWorkspace(workspaceName);
105
+ if (!workspaceData.packageDataEntries.find(x => x.deployment)) {
106
+ console.error(`Workspace ${workspaceName} does not have deployment data.`);
107
+ process.exit(1);
108
+ }
109
+ if (replicaCount < 1) {
110
+ console.error("Replica count must be at least 1.");
111
+ process.exit(1);
112
+ }
113
+ const { scale: _scale, ...rest } = getImageConfigMap(monorepoEnv, image);
114
+ const parsedScale = deserializeImageConfigMapKey<number>(monorepoEnv, image, "scale");
115
+ const isApplicable = setK8sScale(monorepoEnv, workspaceName, replicaCount);
116
+ if (!isApplicable) return;
117
+ updateImageConfigMap(monorepoEnv, image, {
118
+ ...rest,
119
+ scale: JSON.stringify({
120
+ ...parsedScale,
121
+ [workspaceName]: replicaCount,
122
+ }),
123
+ });
124
+ return parsedScale?.[workspaceName] ?? 1;
125
+ }
126
+
127
+ export function getWorkspaceScale(monorepoEnv: string, image: string): Record<string, number>;
128
+ export function getWorkspaceScale(monorepoEnv: string, image: string, workspaceName: string): number;
129
+ export function getWorkspaceScale(
130
+ monorepoEnv: string,
131
+ image: string,
132
+ /** If not provided, returns all workspaces */
133
+ workspaceName?: string
134
+ ) {
135
+ const parsedScale = deserializeImageConfigMapKey<number>(monorepoEnv, image, "scale");
136
+ if (!workspaceName) return parsedScale ?? {};
137
+ const _ensureWorkspace = getWorkspace(workspaceName);
138
+ return parsedScale?.[workspaceName] ?? 1;
139
+ }
140
+
141
+ // /** Returns the old scale prior to deletion */
142
+ export function resetWorkspaceScale(
143
+ monorepoEnv: string,
144
+ image: string,
145
+ workspaceName?: string
146
+ ) {
147
+ const { scale: _scale, ...rest } = getImageConfigMap(monorepoEnv, image);
148
+ const parsedScale = deserializeImageConfigMapKey<number>(monorepoEnv, image, "scale");
149
+ if (!workspaceName) {
150
+ updateImageConfigMap(monorepoEnv, image, rest);
151
+ Object.entries(parsedScale ?? {})
152
+ .filter(([_name, scale]) => Number(scale) > 1)
153
+ .forEach(([name, _scale]) => {
154
+ setK8sScale(monorepoEnv, name, 1);
155
+ });
156
+ return parsedScale;
157
+ } else {
158
+ const oldScale = parsedScale?.[workspaceName] ?? 1;
159
+ const newScale = { ...parsedScale };
160
+ delete newScale[workspaceName];
161
+ updateImageConfigMap(monorepoEnv, image, { ...rest, scale: JSON.stringify(newScale) });
162
+ setK8sScale(monorepoEnv, workspaceName, 1);
163
+ return oldScale;
164
+ }
165
+ }
@@ -0,0 +1,124 @@
1
+ import { CommandExecutor } from "../cli/common";
2
+ import { envToNamespace } from "./k8s-constants";
3
+ import { kubectlCommand } from "./k8s-helpers";
4
+
5
+ const POLL_INTERVAL_SEC = 1;
6
+
7
+ export type JobStatuses = Record<
8
+ string,
9
+ { status: "success" | "failure" | "timeout"; elapsed: number; error?: string }
10
+ >;
11
+
12
+ class K8sJobWaiter {
13
+ timeoutInMs: number;
14
+ namespace: string;
15
+ jobStatuses: JobStatuses;
16
+
17
+ constructor(public monorepoEnv: string, timeoutInS: number) {
18
+ this.namespace = envToNamespace(monorepoEnv);
19
+ this.jobStatuses = {};
20
+ this.timeoutInMs = timeoutInS * 1000;
21
+ }
22
+
23
+ async pollJob(job: string) {
24
+ const startTime = Date.now();
25
+
26
+ while (true) {
27
+ const res = await this.fetchStatus(job);
28
+ const elapsed = Date.now() - startTime;
29
+
30
+ if (res.failure > 0) {
31
+ const error = await this.fetchError(job);
32
+ this.jobStatuses[job] = { status: "failure", elapsed, error };
33
+ return;
34
+ }
35
+
36
+ if (res.success > 0) {
37
+ this.jobStatuses[job] = { status: "success", elapsed };
38
+ return;
39
+ }
40
+
41
+ if (elapsed >= this.timeoutInMs) {
42
+ this.jobStatuses[job] = { status: "timeout", elapsed };
43
+ return;
44
+ }
45
+
46
+ await new Promise((res) => setTimeout(res, POLL_INTERVAL_SEC * 1000));
47
+ }
48
+ }
49
+
50
+ private async fetchStatus(job: string) {
51
+ const result = new CommandExecutor(
52
+ kubectlCommand(
53
+ `get job ${job} -o jsonpath='{.status.failed},{.status.succeeded}'`,
54
+ { namespace: this.namespace }
55
+ )
56
+ ).exec();
57
+ const [failure, success] = result.split(",").map((x: string) => Number(x));
58
+ return { failure, success };
59
+ }
60
+
61
+ private async fetchError(job: string) {
62
+ try {
63
+ const podName = new CommandExecutor(
64
+ kubectlCommand(`get pod -l job-name=${job} -o name`, {
65
+ namespace: this.namespace,
66
+ })
67
+ ).exec();
68
+ const logs = new CommandExecutor(
69
+ kubectlCommand(`logs ${podName}`, { namespace: this.namespace })
70
+ ).exec();
71
+ return logs;
72
+ } catch (e) {
73
+ console.log("Error fetching logs for job", { job, e });
74
+ return `<COULD NOT FETCH ERROR FOR ${job}>`;
75
+ }
76
+ }
77
+ }
78
+
79
+ export async function k8sJobWaiter(
80
+ monorepoEnv: string,
81
+ timeoutInS: number,
82
+ jobs: string[]
83
+ ) {
84
+ const waiter = new K8sJobWaiter(monorepoEnv, timeoutInS);
85
+ await Promise.all(jobs.map((job) => waiter.pollJob(job)));
86
+ return waiter.jobStatuses;
87
+ }
88
+
89
+ export function printJobStatuses(statuses: JobStatuses) {
90
+ console.log("Statuses:");
91
+ Object.entries(statuses).forEach(([job, statusRecord]) => {
92
+ console.log(
93
+ `${job}: ${statusRecord.status} ${
94
+ Math.round((statusRecord.elapsed / 1000) * 100) / 100
95
+ }s`
96
+ );
97
+ });
98
+ console.log();
99
+
100
+ Object.entries(statuses)
101
+ .filter(([_, statusRecord]) => statusRecord.status === "failure")
102
+ .forEach(([job, statusRecord]) => {
103
+ console.error(`Error for ${job}:`);
104
+ console.error(statusRecord.error);
105
+ console.error();
106
+ });
107
+
108
+ const failures = Object.entries(statuses).filter(([_, statusRecord]) =>
109
+ ["failure", "timeout"].includes(statusRecord.status)
110
+ );
111
+
112
+ if (failures.length > 0) {
113
+ console.error();
114
+ console.error(
115
+ `Some jobs did not succeed: ${failures.map(([job, _]) => job).join(", ")}`
116
+ );
117
+ console.error();
118
+ process.exit(1);
119
+ }
120
+
121
+ Object.entries(statuses).forEach(([job, statusRecord]) => {
122
+ console.log(`${job}: ${statusRecord.status}`);
123
+ });
124
+ }
@@ -0,0 +1,41 @@
1
+ import { CommandExecutor } from "../cli/common";
2
+ import { envToNamespace, secretName } from "./k8s-constants";
3
+ import { kubectlCommand, patchSecretKeyCommand } from "./k8s-helpers";
4
+ import { randomBytes } from "crypto";
5
+
6
+ export const BASE_SECRET_KEY = 'baseSecret';
7
+
8
+ export function checkEnvSetup(monorepoEnv: string) {
9
+ const namespace = envToNamespace(monorepoEnv);
10
+ const exitCode = new CommandExecutor(
11
+ kubectlCommand(`get ns ${namespace}`)
12
+ ).exec({
13
+ onlyStatusCode: true,
14
+ });
15
+ return exitCode === 0;
16
+ }
17
+
18
+ export function createNamespace(monorepoEnv: string) {
19
+ new CommandExecutor(kubectlCommand(`create ns ${envToNamespace(monorepoEnv)}`)).exec();
20
+ }
21
+
22
+ export function createEmptyEnvSecret(monorepoEnv: string) {
23
+ const cmd = kubectlCommand(`create secret generic ${secretName()}`, {namespace: envToNamespace(monorepoEnv)});
24
+ new CommandExecutor(cmd).exec();
25
+ }
26
+
27
+ export function patchBaseSecret(monorepoEnv: string) {
28
+ const { fullCommand, redactedCommand } = patchSecretKeyCommand(
29
+ monorepoEnv,
30
+ secretName(),
31
+ BASE_SECRET_KEY,
32
+ randomBytes(32).toString('hex')
33
+ );
34
+ new CommandExecutor(fullCommand, { quiet: true, redactedCommand }).exec();
35
+ }
36
+
37
+ export function deleteNamespace(monorepoEnv: string) {
38
+ const cmd = kubectlCommand(`delete ns ${envToNamespace(monorepoEnv)}`);
39
+ new CommandExecutor(cmd).exec();
40
+ }
41
+
@@ -0,0 +1,31 @@
1
+ import { CommandExecutor } from "../cli/common";
2
+ import { kubectlCommand } from "./k8s-helpers";
3
+
4
+ export function getRedisList() {
5
+ const cmd = kubectlCommand(`get pods -l app.kubernetes.io/name=redis -A`);
6
+ const res = new CommandExecutor(cmd, { quiet: true }).exec();
7
+ if (!res) return null;
8
+ return res;
9
+ }
10
+
11
+ export function getRedisPassword(namespace: string) {
12
+ const cmd = kubectlCommand(`get secrets/${namespace} -o jsonpath="{.data}"`, {
13
+ namespace,
14
+ });
15
+ const res = new CommandExecutor(cmd, { quiet: true }).exec();
16
+ if (!res) return null;
17
+ try {
18
+ const resJson = JSON.parse(res);
19
+ const password = atob(resJson["redis-password"]);
20
+ return { password };
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ export function establishRedisTunnel(namespace: string, port: string) {
27
+ const cmd = kubectlCommand(`port-forward svc/${namespace}-master ${port}:6379`, {
28
+ namespace,
29
+ });
30
+ new CommandExecutor(cmd).spawn();
31
+ }
@@ -0,0 +1,79 @@
1
+ import { CommandExecutor } from "../cli/common";
2
+ import { secretName } from "./k8s-constants";
3
+ import { kubectlCommand, patchSecretKeyCommand } from "./k8s-helpers";
4
+
5
+ // This env file should not be used in local development
6
+ const SECRET_FILE_NAME = "env_json";
7
+
8
+ // Basic commands (L1)
9
+
10
+ // Override the secret value with the new value
11
+ function execUpdateSecret(
12
+ monorepoEnv: string,
13
+ secretValue: Record<string, string>
14
+ ) {
15
+ const { fullCommand, redactedCommand } = patchSecretKeyCommand(monorepoEnv, secretName(), SECRET_FILE_NAME, JSON.stringify(secretValue));
16
+ new CommandExecutor(fullCommand, { quiet: true, redactedCommand }).exec();
17
+ }
18
+
19
+ function getSecret(monorepoEnv: string, keys: string[] = []) {
20
+ // Dots in jsonpath can only be accessed with a \ prefix
21
+ const escapedSecretFileName = SECRET_FILE_NAME.replaceAll(".", "\\.");
22
+ // prettier-ignore
23
+ const cmd = kubectlCommand(`get secrets/${secretName()} -o jsonpath="{.data['${escapedSecretFileName}']}"`, { monorepoEnv });
24
+ const res = new CommandExecutor(cmd, { quiet: true }).exec();
25
+ if (!res) return {};
26
+ const resJson: Record<string, string> = JSON.parse(atob(res));
27
+ if (!keys || keys.length === 0) return resJson;
28
+ return Object.fromEntries(
29
+ keys.filter((x) => resJson[x]).map((x) => [x, resJson[x]])
30
+ );
31
+ }
32
+
33
+ // Combined commands (L2)
34
+
35
+ function updateSecret(monorepoEnv: string, vars: Record<string, string>) {
36
+ if (!vars || Object.keys(vars).length === 0) {
37
+ console.error(
38
+ "Keys-value pairs to set must be provided, e.g. KEY1=val1 KEY2=val2"
39
+ );
40
+ process.exit(1);
41
+ }
42
+ const current = getSecret(monorepoEnv);
43
+ const newVars = { ...current, ...vars };
44
+ execUpdateSecret(monorepoEnv, newVars);
45
+ }
46
+
47
+ function deleteSecretKeys(monorepoEnv: string, keys: string[] = []) {
48
+ if (!keys || keys.length === 0) {
49
+ console.error("Keys to delete must be provided");
50
+ process.exit(1);
51
+ }
52
+ const secretValue = getSecret(monorepoEnv);
53
+ keys.forEach((key) => delete secretValue[key]);
54
+ execUpdateSecret(monorepoEnv, secretValue);
55
+ }
56
+
57
+ //= Interface (L3)
58
+
59
+ export function getMonorepoSecret(monorepoEnv: string, keys: string[] = []) {
60
+ const value = getSecret(monorepoEnv, keys);
61
+ return Object.entries(value)
62
+ .map((pair) => pair.join("="))
63
+ .join("\n");
64
+ }
65
+
66
+ /** E.g.: setMonorepoSecret('staging', ['KEY1=val1', 'KEY2=val2']) */
67
+ export function setMonorepoSecret(monorepoEnv: string, pairs: string[] = []) {
68
+ const pairsObj = Object.fromEntries(
69
+ pairs.map((x) => {
70
+ const [key, ...values] = x.split("=");
71
+ return [key, values.join("=")];
72
+ })
73
+ );
74
+ updateSecret(monorepoEnv, pairsObj);
75
+ }
76
+
77
+ export function deleteMonorepoSecret(monorepoEnv: string, keys: string[] = []) {
78
+ deleteSecretKeys(monorepoEnv, keys);
79
+ }
@@ -0,0 +1,223 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ CombinedEnvValidator,
4
+ DotEnvParser,
5
+ SingleEnvValidator,
6
+ } from './validate-env';
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
9
+ function withEnvVars(envVars: Record<string, string>, callbackFn: Function) {
10
+ Object.entries(envVars).forEach(([key, value]) => (process.env[key] = value));
11
+ try {
12
+ callbackFn();
13
+ } finally {
14
+ Object.keys(envVars).forEach((key) => delete process.env[key]);
15
+ }
16
+ }
17
+
18
+ describe('SingleEnvValidator', () => {
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ function createSubject(yamlContent: any) {
21
+ const subject = new SingleEnvValidator('dummy-path.yaml');
22
+ vi.spyOn(subject, '_readFile').mockReturnValue(yamlContent);
23
+ return subject;
24
+ }
25
+
26
+ describe('invalid yaml files', () => {
27
+ it('contains parsingError when not an array', () => {
28
+ const subject = createSubject({ DUMMY1: 'optional' });
29
+ subject.validate();
30
+ expect(subject.parsingError).toContain(
31
+ 'env.yaml file must resolve to an array',
32
+ );
33
+ });
34
+
35
+ it('contains parsingError when an object has multiple keys', () => {
36
+ const subject = createSubject([
37
+ { DUMMY1: 'optional', DUMMY2: 'optional' },
38
+ ]);
39
+ subject.validate();
40
+ expect(subject.parsingError).toContain(
41
+ 'every object in env.yaml must have one key. Error near: DUMMY1',
42
+ );
43
+ });
44
+
45
+ it('contains parsingError when an object has invalid requirement', () => {
46
+ const subject = createSubject([{ DUMMY1: 'optttttttional' }]);
47
+ subject.validate();
48
+ expect(subject.parsingError).toContain('invalid value for DUMMY1');
49
+ });
50
+ });
51
+
52
+ describe('valid yaml file', () => {
53
+ const validYaml = [
54
+ 'TEST_ENV_MANDATORY',
55
+ { TEST_ENV_OPTIONAL: 'optional' },
56
+ { TEST_ENV_BOOLEAN1: 'boolean' },
57
+ { TEST_ENV_BOOLEAN2: 'boolean' },
58
+ { TEST_ENV_ENUM1: ['option1', 'option2'] },
59
+ { TEST_ENV_ENUM2: ['option1', 'option2'] },
60
+ ];
61
+
62
+ const defaultValues = {
63
+ TEST_ENV_MANDATORY: 'some_value',
64
+ TEST_ENV_BOOLEAN1: 'true',
65
+ TEST_ENV_BOOLEAN2: 'false',
66
+ TEST_ENV_ENUM1: 'option1',
67
+ TEST_ENV_ENUM2: 'option2',
68
+ };
69
+
70
+ function withEnvVarsHelper(
71
+ envVars: Partial<
72
+ Record<keyof typeof defaultValues, string> & {
73
+ TEST_ENV_OPTIONAL: string;
74
+ }
75
+ >,
76
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
77
+ callbackFn: Function,
78
+ ) {
79
+ envVars = { ...defaultValues, ...envVars };
80
+ withEnvVars(envVars, callbackFn);
81
+ }
82
+
83
+ it('works when all env vars are provided as expected', () => {
84
+ const subject = createSubject(validYaml);
85
+ withEnvVarsHelper({}, () => subject.validate());
86
+ expect(subject.parsingError).toEqual(undefined);
87
+ expect(subject.errors).toEqual({});
88
+ });
89
+
90
+ it('works when an optional varilable is provided', () => {
91
+ const subject = createSubject(validYaml);
92
+ withEnvVarsHelper({ TEST_ENV_OPTIONAL: 'value' }, () =>
93
+ subject.validate(),
94
+ );
95
+ expect(subject.parsingError).toEqual(undefined);
96
+ expect(subject.errors).toEqual({});
97
+ });
98
+
99
+ it('contains errors when mandatory is not provided', () => {
100
+ const subject = createSubject(validYaml);
101
+ withEnvVarsHelper({ TEST_ENV_MANDATORY: '' }, () => subject.validate());
102
+ expect(subject.parsingError).toEqual(undefined);
103
+ expect(Object.keys(subject.errors)).toEqual(['TEST_ENV_MANDATORY']);
104
+ });
105
+
106
+ it('contains errors when boolean is different than true or false', () => {
107
+ const subject = createSubject(validYaml);
108
+ withEnvVarsHelper({ TEST_ENV_BOOLEAN1: 'value' }, () =>
109
+ subject.validate(),
110
+ );
111
+ expect(subject.parsingError).toEqual(undefined);
112
+ expect(Object.keys(subject.errors)).toEqual(['TEST_ENV_BOOLEAN1']);
113
+ });
114
+
115
+ it('contains errors when enum is different than provided options', () => {
116
+ const subject = createSubject(validYaml);
117
+ withEnvVarsHelper({ TEST_ENV_ENUM1: 'option3' }, () =>
118
+ subject.validate(),
119
+ );
120
+ expect(subject.parsingError).toEqual(undefined);
121
+ expect(Object.keys(subject.errors)).toEqual(['TEST_ENV_ENUM1']);
122
+ });
123
+ });
124
+ });
125
+
126
+ describe('DotEnvParser', () => {
127
+ function createSubject(text: string) {
128
+ const subject = new DotEnvParser('dummy-path.env');
129
+ vi.spyOn(subject, '_readFile').mockReturnValue(text);
130
+ return subject;
131
+ }
132
+
133
+ it('works, taking into account comments and ignoring empty lines', () => {
134
+ const subject = createSubject(`
135
+
136
+ # Ignored
137
+ TEST1=hello
138
+
139
+ TEST2=world # also ignored
140
+
141
+
142
+ TEST3 = spaces should not normally be allowed in .env # so the parser is not strict
143
+
144
+ `);
145
+ subject.parse();
146
+ expect(subject.keys).toEqual(['TEST1', 'TEST2', 'TEST3']);
147
+ });
148
+ });
149
+
150
+ describe('CombinedEnvValidator', () => {
151
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
+ function createSubject(yamlContent: any[], dotEnvContent: string[]) {
153
+ const yamlPaths = yamlContent.map((_, i) => `dummy-yaml-${i}`);
154
+ const dotEnvPaths = dotEnvContent.map((_, i) => `dummy-dotenv-${i}`);
155
+
156
+ const yamlValidators = yamlContent.map((content, i) => {
157
+ const validator = new SingleEnvValidator(yamlPaths[i]);
158
+ vi.spyOn(validator, '_readFile').mockReturnValue(content);
159
+ return validator;
160
+ });
161
+
162
+ const dotEnvParsers = dotEnvContent.map((content, i) => {
163
+ const parser = new DotEnvParser(dotEnvPaths[i]);
164
+ vi.spyOn(parser, '_readFile').mockReturnValue(content);
165
+ return parser;
166
+ });
167
+
168
+ const subject = new CombinedEnvValidator(yamlPaths, dotEnvPaths);
169
+ vi.spyOn(subject, '_loadYamlFiles').mockImplementation(
170
+ () => (subject.yamlValidators = yamlValidators),
171
+ );
172
+ vi.spyOn(subject, '_loadDotEnvFiles').mockImplementation(
173
+ () => (subject.dotEnvParsers = dotEnvParsers),
174
+ );
175
+ return subject;
176
+ }
177
+
178
+ it('works when no errors and no warnings', () => {
179
+ const yaml1 = ['TEST_ENV1', 'TEST_ENV2'];
180
+ const yaml2 = [{ TEST_ENV3: 'optional' }];
181
+ const dotEnv1 = 'TEST_ENV1=hi';
182
+ const dotEnv2 = 'TEST_ENV2=bye';
183
+
184
+ const subject = createSubject([yaml1, yaml2], [dotEnv1, dotEnv2]);
185
+ withEnvVars({ TEST_ENV1: 'hi', TEST_ENV2: 'bye' }, () =>
186
+ subject.validate(),
187
+ );
188
+
189
+ expect(subject.warnings).toEqual([]);
190
+ expect(subject.errors).toEqual({});
191
+ });
192
+
193
+ it('emits warnings', () => {
194
+ const yaml1 = ['TEST_ENV1', 'TEST_ENV2'];
195
+ const yaml2 = [{ TEST_ENV3: 'optional' }];
196
+ const dotEnv1 = 'TEST_ENV1=hi';
197
+ const dotEnv2 = 'TEST_ENV2=bye';
198
+ const dotEnv3 = 'TEST_ENV4=boo';
199
+
200
+ const subject = createSubject([yaml1, yaml2], [dotEnv1, dotEnv2, dotEnv3]);
201
+ withEnvVars({ TEST_ENV1: 'hi', TEST_ENV2: 'bye' }, () =>
202
+ subject.validate(),
203
+ );
204
+
205
+ expect(subject.warnings.length).toEqual(1);
206
+ expect(subject.warnings[0]).toContain('TEST_ENV4');
207
+ expect(subject.errors).toEqual({});
208
+ });
209
+
210
+ it('emits errors', () => {
211
+ vi.spyOn(process, 'exit').mockReturnValue(undefined as never);
212
+ const yaml1 = ['TEST_ENV1', 'TEST_ENV2'];
213
+ const yaml2 = [{ TEST_ENV3: 'optional' }];
214
+ const dotEnv1 = 'TEST_ENV1=hi';
215
+
216
+ const subject = createSubject([yaml1, yaml2], [dotEnv1]);
217
+ withEnvVars({ TEST_ENV1: 'hi' }, () => subject.validate());
218
+ expect(process.exit).toHaveBeenCalledWith(1);
219
+
220
+ expect(subject.warnings).toEqual([]);
221
+ expect(Object.keys(subject.errors)).toEqual(['TEST_ENV2']);
222
+ });
223
+ });